feat: Implement clipboard manager commands and history management
- Added command to clear clipboard history (`cmd_clear.go`). - Implemented command to copy a history item back to the clipboard (`cmd_copy.go`). - Created command to list clipboard history (`cmd_list.go`). - Developed command to watch clipboard changes and store history (`cmd_watch.go`). - Introduced configuration loading for logging and clipboard settings (`config.go`). - Established main application logic with command registration and configuration handling (`main.go`). - Implemented history management with loading, saving, and clearing functionality (`history.go`). - Defined history item structure to store clipboard data (`history_item.go`). - Added utility functions for hashing data and summarizing clipboard content (`hash.go`, `summary.go`). - Updated dependencies in `go.sum`.
This commit is contained in:
commit
1dad8ca69b
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/src/main.go",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"args": [
|
||||
"debug",
|
||||
],
|
||||
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": "**/docker/**/*.yml"
|
||||
}
|
||||
}
|
20
Makefile
Normal file
20
Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# Define paths and installation directories
|
||||
BINARY_NAME := kcm
|
||||
BINARY_PATH := bin/$(BINARY_NAME)
|
||||
COMPLETION_SCRIPT := bin/${BINARY_NAME}-completion.bash
|
||||
|
||||
# Build the Go application
|
||||
build: clean
|
||||
@bin/scripts/build-binary.sh $(BINARY_NAME) $(BINARY_PATH) $(COMPLETION_SCRIPT)
|
||||
|
||||
clean:
|
||||
@bin/scripts/clean.sh $(BINARY_PATH) $(COMPLETION_SCRIPT)
|
||||
|
||||
install:
|
||||
@bin/scripts/install.sh
|
||||
|
||||
install-global:
|
||||
@bin/scripts/install-global.sh
|
||||
|
||||
uninstall:
|
||||
@bin/scripts/uninstall.sh
|
186
bin/helpers/func.sh
Executable file
186
bin/helpers/func.sh
Executable file
@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#Color print function, usage: println "message" "color"
|
||||
println() {
|
||||
color=$2
|
||||
printfe "%s\n" $color "$1"
|
||||
}
|
||||
|
||||
# print colored with printf (args: format, color, message ...)
|
||||
printfe() {
|
||||
format=$1
|
||||
color=$2
|
||||
message=$3
|
||||
show_time=true
|
||||
|
||||
# Check if $4 is explicitly set to false, otherwise default to true
|
||||
if [ ! -z "$4" ] && [ "$4" == "false" ]; then
|
||||
show_time=false
|
||||
fi
|
||||
|
||||
red=$(tput setaf 1)
|
||||
green=$(tput setaf 2)
|
||||
yellow=$(tput setaf 3)
|
||||
blue=$(tput setaf 4)
|
||||
magenta=$(tput setaf 5)
|
||||
cyan=$(tput setaf 6)
|
||||
normal=$(tput sgr0)
|
||||
grey=$(tput setaf 8)
|
||||
|
||||
case $color in
|
||||
"red")
|
||||
color=$red
|
||||
;;
|
||||
"green")
|
||||
color=$green
|
||||
;;
|
||||
"yellow")
|
||||
color=$yellow
|
||||
;;
|
||||
"blue")
|
||||
color=$blue
|
||||
;;
|
||||
"magenta")
|
||||
color=$magenta
|
||||
;;
|
||||
"cyan")
|
||||
color=$cyan
|
||||
;;
|
||||
"grey")
|
||||
color=$grey
|
||||
;;
|
||||
*)
|
||||
color=$normal
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$show_time" == "false" ]; then
|
||||
printf "$color$format$normal" "$message"
|
||||
return
|
||||
fi
|
||||
|
||||
printf $grey"%s" "$(date +'%H:%M:%S')"$normal
|
||||
|
||||
case $color in
|
||||
$green | $cyan | $blue | $magenta | $normal)
|
||||
printf "$green INF $normal"
|
||||
;;
|
||||
$yellow)
|
||||
printf "$yellow WRN $normal"
|
||||
;;
|
||||
$red)
|
||||
printf "$red ERR $normal"
|
||||
;;
|
||||
*)
|
||||
printf "$normal"
|
||||
;;
|
||||
esac
|
||||
printf "$color$format$normal" "$message"
|
||||
}
|
||||
|
||||
run_docker_command() {
|
||||
cmd=$1
|
||||
log_level="$2"
|
||||
shift
|
||||
shift
|
||||
params=$@
|
||||
composer_image="composer/composer:2.7.8"
|
||||
php_image="php:8.3-cli-alpine3.20"
|
||||
phpstan_image="atishoo/phpstan:latest"
|
||||
AUTH_SOCK_DIRNAME=$(dirname $SSH_AUTH_SOCK)
|
||||
|
||||
# It's possible $SSH_AUTH_SOCK is not set, in that case we should set it to /tmp/ssh_auth_sock
|
||||
if [ -z "$SSH_AUTH_SOCK" ]; then
|
||||
AUTH_SOCK_DIRNAME="/tmp/ssh_auth_sock:/tmp/ssh_auth_sock"
|
||||
fi
|
||||
|
||||
# Take the name of the current directory
|
||||
container=$(basename $(pwd) | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check if the $container is an actual container from the $TRADAWARE_PATH/docker-compose.yml
|
||||
result=$(docker compose -f $TRADAWARE_PATH/docker-compose.yml ps -q $container 2>/dev/null)
|
||||
if [ -z "$result" ]; then
|
||||
# Ensure /home/$USER/.config/composer/auth.json exists, if not prefill it with an empty JSON object
|
||||
if [ ! -f /home/$USER/.config/composer/auth.json ]; then
|
||||
mkdir -p /home/$USER/.config/composer
|
||||
touch /home/$USER/.config/composer/auth.json
|
||||
echo "{
|
||||
\"github-oauth\": {
|
||||
\"github.com\": \"KEY_HERE\"
|
||||
}
|
||||
}" > /home/$USER/.config/composer/auth.json
|
||||
printfe "%s" "yellow" "Created an empty auth.json file at '"
|
||||
printfe "%s" "cyan" "/home/$USER/.config/composer/auth.json"
|
||||
printfe "%s\n" "yellow" "', you should edit this file and add your GitHub OAuth key."
|
||||
return
|
||||
fi
|
||||
|
||||
# In case cmd is composer run it with composer image
|
||||
if [ "$cmd" == "composer" ]; then
|
||||
if [ "$log_level" == "0" ] || [ "$log_level" == "-1" ]; then
|
||||
printfe "%s" "cyan" "Running '"
|
||||
printfe "%s" "yellow" "$cmd $params"
|
||||
printfe "%s" "cyan" "' in "
|
||||
printfe "%s" "yellow" "'$composer_image'"
|
||||
printfe "%s\n" "cyan" " container..."
|
||||
fi
|
||||
|
||||
docker run --rm --interactive --tty \
|
||||
--volume $PWD:/app \
|
||||
--volume $AUTH_SOCK_DIRNAME \
|
||||
--volume /etc/passwd:/etc/passwd:ro \
|
||||
--volume /etc/group:/etc/group:ro \
|
||||
--volume /home/$USER/.ssh:/root/.ssh \
|
||||
--volume /home/$USER/.config/composer/auth.json:/tmp/auth.json \
|
||||
--env SSH_AUTH_SOCK=$SSH_AUTH_SOCK \
|
||||
--user $(id -u):$(id -g) \
|
||||
$composer_image $cmd $params
|
||||
elif [ "$cmd" == "php" ]; then
|
||||
if [ "$log_level" == "0" ] || [ "$log_level" == "-1" ]; then
|
||||
printfe "%s" "cyan" "Running '"
|
||||
printfe "%s" "yellow" "$cmd $params"
|
||||
printfe "%s" "cyan" "' in "
|
||||
printfe "%s" "yellow" "'$php_image'"
|
||||
printfe "%s\n" "cyan" " container..."
|
||||
fi
|
||||
|
||||
docker run --rm --interactive --tty \
|
||||
--volume $PWD:/app \
|
||||
--volume $AUTH_SOCK_DIRNAME \
|
||||
--volume /etc/passwd:/etc/passwd:ro \
|
||||
--volume /etc/group:/etc/group:ro \
|
||||
--volume /home/$USER/.ssh:/root/.ssh \
|
||||
--volume /home/$USER/.config/composer/auth.json:/tmp/auth.json \
|
||||
--env SSH_AUTH_SOCK=$SSH_AUTH_SOCK \
|
||||
--user $(id -u):$(id -g) \
|
||||
$php_image $cmd $params
|
||||
elif [ "$cmd" == "phpstan" ]; then
|
||||
if [ "$log_level" == "0" ] || [ "$log_level" == "-1" ]; then
|
||||
printfe "%s" "cyan" "Running '"
|
||||
printfe "%s" "yellow" "$cmd $params"
|
||||
printfe "%s" "cyan" "' in "
|
||||
printfe "%s" "yellow" "'$phpstan_image'"
|
||||
printfe "%s\n" "cyan" " container..."
|
||||
fi
|
||||
|
||||
docker run --rm --interactive --tty \
|
||||
--volume $PWD:/app \
|
||||
--user $(id -u):$(id -g) \
|
||||
$phpstan_image $params
|
||||
else
|
||||
println "No container found named $container and given command is not composer or php." "red"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
docker_user=docker
|
||||
|
||||
if [ "$log_level" == "0" ] || [ "$log_level" == "-1" ]; then
|
||||
printfe "%s" "cyan" "Running '"
|
||||
printfe "%s" "yellow" "$cmd $params"
|
||||
printfe "%s" "cyan" "' in "
|
||||
printfe "%s" "yellow" "'$container'"
|
||||
printfe "%s\n" "cyan" " container..."
|
||||
fi
|
||||
docker compose -f $TRADAWARE_PATH/docker-compose.yml exec -u $docker_user --interactive --tty $container $cmd $params
|
||||
}
|
48
bin/scripts/build-binary.sh
Executable file
48
bin/scripts/build-binary.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BINARY_NAME=$1
|
||||
BINARY_PATH=$2
|
||||
COMPLETION_SCRIPT=$3
|
||||
BINARY_PATH_VERSION=$BINARY_PATH.version
|
||||
|
||||
source bin/helpers/func.sh
|
||||
|
||||
# Check if HEAD is clean, if not abort
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
printfe "%s\n" "yellow" "You have uncomitted and/or untracked changes in your working directory."
|
||||
fi
|
||||
|
||||
# Get the current tag checked out to HEAD and hash
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
LATEST_COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null)
|
||||
LATEST_TAG_HASH=$(git rev-list -n 1 --abbrev-commit $LATEST_TAG 2>/dev/null)
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
|
||||
# If BRANCH is HEAD and latest commit hash equals latest tag hash, we are on a tag and up to date
|
||||
if [ "$BRANCH" == "HEAD" ] && [ "$LATEST_COMMIT_HASH" == "$LATEST_TAG_HASH" ]; then
|
||||
BRANCH=$LATEST_TAG
|
||||
fi
|
||||
|
||||
# In case the current head has uncomitted and/or untracked changes, append a postfix to the version saying (dirty)
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
POSTFIX=" (dirty)"
|
||||
fi
|
||||
|
||||
printfe "%s\n" "cyan" "Building $BINARY_NAME binary for $BRANCH ($LATEST_COMMIT_HASH)$POSTFIX..."
|
||||
go build -o $BINARY_PATH ./src
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "\033[0;31m"
|
||||
echo "Build failed."
|
||||
printf "\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Put tag and hash in .kcm_version file
|
||||
echo "$BRANCH ($LATEST_COMMIT_HASH)$POSTFIX" > $BINARY_PATH_VERSION
|
||||
|
||||
printfe "%s\n" "cyan" "Generating Bash completion script..."
|
||||
$BINARY_PATH completion bash > $COMPLETION_SCRIPT
|
||||
|
||||
printfe "%s\n" "green" "Bash completion script installed to $COMPLETION_SCRIPT."
|
||||
printfe "%s\n" "green" "Restart or 'source ~/.bashrc' to update your shell."
|
19
bin/scripts/clean.sh
Executable file
19
bin/scripts/clean.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source bin/helpers/func.sh
|
||||
|
||||
# $1 should be binary path
|
||||
BINARY_PATH=$1
|
||||
|
||||
# $2 should be completion script path
|
||||
COMPLETION_SCRIPT=$2
|
||||
|
||||
# Confirm these are paths
|
||||
if [ -z "$BINARY_PATH" ] || [ -z "$COMPLETION_SCRIPT" ]; then
|
||||
printfe "%s\n" "red" "Usage: $0 <binary_path> <completion_script_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printfe "%s\n" "cyan" "Cleaning up old binaries and completion scripts..."
|
||||
rm -f $BINARY_PATH
|
||||
rm -f $COMPLETION_SCRIPT
|
27
bin/scripts/install-global.sh
Executable file
27
bin/scripts/install-global.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source bin/helpers/func.sh
|
||||
|
||||
# Remove any existing kcm installation
|
||||
if [ -f "/usr/local/bin/kcm" ]; then
|
||||
printfe "%s\n" "yellow" "Removing existing kcm installation..."
|
||||
rm /usr/local/bin/kcm
|
||||
fi
|
||||
|
||||
if [ -f "/usr/share/bash-completion/completions/kcm" ]; then
|
||||
printfe "%s\n" "yellow" "Removing existing kcm bash completion..."
|
||||
rm /usr/share/bash-completion/completions/kcm
|
||||
fi
|
||||
|
||||
# Copy binary files to /usr/local/bin
|
||||
printfe "%s\n" "cyan" "Installing kcm..."
|
||||
cp $(pwd)/bin/kcm /usr/local/bin/kcm
|
||||
cp $(pwd)/bin/kcm-completion.bash /usr/share/bash-completion/completions/kcm
|
||||
|
||||
# In case /etc/kcm/config.yaml does not exist, create it
|
||||
if [ ! -f "/etc/kcm/config.local.yaml" ]; then
|
||||
printfe "%s\n" "cyan" "Creating default configuration file..."
|
||||
mkdir -p /etc/kcm
|
||||
cp $(pwd)/config/config.local.example.yaml /etc/kcm/config.local.yaml
|
||||
fi
|
||||
printfe "%s\n" "green" "Installation complete."
|
16
bin/scripts/install.sh
Executable file
16
bin/scripts/install.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Create any missing directories/files
|
||||
touch ~/.bash_completion
|
||||
mkdir -p $HOME/.local/bin/
|
||||
|
||||
# Symbolically link binaries
|
||||
ln -sf $(pwd)/bin/kcm $HOME/.local/bin/kcm
|
||||
ln -sf $(pwd)/bin/kcm-completion.bash $HOME/.local/bin/kcm-completion.bash
|
||||
|
||||
# Add completion to bash_completion for kcm
|
||||
sed -i '/kcm/d' ~/.bash_completion
|
||||
echo "source $HOME/.local/bin/kcm-completion.bash" >> ~/.bash_completion
|
||||
|
||||
echo "Installation complete."
|
||||
source ~/.bash_completion
|
29
bin/scripts/uninstall.sh
Executable file
29
bin/scripts/uninstall.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/usr/env bash
|
||||
|
||||
if [ -f $HOME/.local/bin/kcm ]; then
|
||||
echo "Removing kcm from $HOME/.local/bin"
|
||||
rm $HOME/.local/bin/kcm
|
||||
rm $HOME/.local/bin/T
|
||||
fi
|
||||
|
||||
if [ -f $HOME/.local/bin/kcm-completion.bash ]; then
|
||||
echo "Removing kcm-completion.bash from $HOME/.local/bin"
|
||||
rm $HOME/.local/bin/kcm-completion.bash
|
||||
fi
|
||||
|
||||
if [ -f $HOME/.local/bin/php ]; then
|
||||
echo "Removing php from $HOME/.local/bin"
|
||||
rm $HOME/.local/bin/php
|
||||
fi
|
||||
|
||||
if [ -f $HOME/.local/bin/composer ]; then
|
||||
echo "Removing composer from $HOME/.local/bin"
|
||||
rm $HOME/.local/bin/composer
|
||||
fi
|
||||
|
||||
if [ -f $HOME/.local/bin/phpstan ]; then
|
||||
echo "Removing phpstan from $HOME/.local/bin"
|
||||
rm $HOME/.local/bin/phpstan
|
||||
fi
|
||||
|
||||
echo "Uninstall complete."
|
13
config.yml
Normal file
13
config.yml
Normal file
@ -0,0 +1,13 @@
|
||||
# Example config file for clipboard manager
|
||||
logging:
|
||||
# Format of the log output. Can be 'console' or 'json'.
|
||||
# 'console' will output to the console, 'json' will output in JSON format.
|
||||
# 'json' is useful for structured logging and can be parsed by log management systems.
|
||||
format: console
|
||||
|
||||
# Logging level. Can be 'debug', 'info', 'warning', 'error', or 'critical'.
|
||||
level: info
|
||||
|
||||
clipboard:
|
||||
# Maximum number of items to keep in the clipboard history.
|
||||
max_items: 100
|
50
go.mod
Normal file
50
go.mod
Normal file
@ -0,0 +1,50 @@
|
||||
module github.com/vleeuwenmenno/kcm
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/rs/zerolog v1.34.0
|
||||
golang.design/x/clipboard v0.7.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.1 // indirect
|
||||
fyne.io/systray v1.11.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.1.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.2.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
140
go.sum
Normal file
140
go.sum
Normal file
@ -0,0 +1,140 @@
|
||||
fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
|
||||
fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
|
||||
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
|
||||
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo=
|
||||
golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
20
src/commands/cmd_clear.go
Normal file
20
src/commands/cmd_clear.go
Normal file
@ -0,0 +1,20 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vleeuwenmenno/kcm/src/models"
|
||||
)
|
||||
|
||||
func NewClearCmd(history *models.History) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "clear",
|
||||
Short: "Clear clipboard history",
|
||||
Aliases: []string{"--clear"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
history.Clear()
|
||||
fmt.Println("Clipboard history cleared.")
|
||||
},
|
||||
}
|
||||
}
|
39
src/commands/cmd_copy.go
Normal file
39
src/commands/cmd_copy.go
Normal file
@ -0,0 +1,39 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vleeuwenmenno/kcm/src/models"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
func NewCopyCmd(history *models.History) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "copy [index]",
|
||||
Short: "Copy a history item back to the clipboard",
|
||||
Aliases: []string{"--copy"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
index, err := strconv.Atoi(args[0])
|
||||
if err != nil || index < 1 {
|
||||
fmt.Println("Invalid index. Use the number shown in 'kcm list'.")
|
||||
return
|
||||
}
|
||||
history.ReloadIfChanged()
|
||||
historyLen := len(history.Items)
|
||||
if index > historyLen {
|
||||
fmt.Printf("Index out of range. There are only %d items.\n", historyLen)
|
||||
return
|
||||
}
|
||||
item := history.Items[index-1]
|
||||
if err := clipboard.Init(); err != nil {
|
||||
fmt.Println("Failed to initialize clipboard:", err)
|
||||
return
|
||||
}
|
||||
clipboard.Write(item.DataType, item.Data)
|
||||
fmt.Printf("Copied item %d to clipboard.\n", index)
|
||||
},
|
||||
}
|
||||
}
|
20
src/commands/cmd_list.go
Normal file
20
src/commands/cmd_list.go
Normal file
@ -0,0 +1,20 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vleeuwenmenno/kcm/src/models"
|
||||
)
|
||||
|
||||
func NewListCmd(history *models.History) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List clipboard history",
|
||||
Aliases: []string{"--list"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
history.ReloadIfChanged()
|
||||
history.List(os.Stdout)
|
||||
},
|
||||
}
|
||||
}
|
64
src/commands/cmd_watch.go
Normal file
64
src/commands/cmd_watch.go
Normal file
@ -0,0 +1,64 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vleeuwenmenno/kcm/src/models"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
func NewWatchCmd(history *models.History) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "watch",
|
||||
Short: "Watch clipboard and store history",
|
||||
Aliases: []string{"--watch"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info().Msg("Starting clipboard watcher (golang.design/x/clipboard)...")
|
||||
|
||||
if err := clipboard.Init(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize clipboard")
|
||||
}
|
||||
|
||||
var (
|
||||
lastText string
|
||||
lastImage []byte
|
||||
)
|
||||
|
||||
for {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
textOut := clipboard.Read(clipboard.FmtText)
|
||||
imgOut := clipboard.Read(clipboard.FmtImage)
|
||||
|
||||
if len(textOut) > 0 {
|
||||
text := string(textOut)
|
||||
if text != lastText {
|
||||
item := models.HistoryItem{
|
||||
Data: []byte(text),
|
||||
DataType: 0, // 0 for text
|
||||
Timestamp: time.Now(),
|
||||
Pinned: false,
|
||||
}
|
||||
history.Add(item)
|
||||
lastText = text
|
||||
log.Info().Str("content", text).Msg("Text clipboard item added to history")
|
||||
}
|
||||
}
|
||||
|
||||
if len(imgOut) > 0 && (lastImage == nil || string(imgOut) != string(lastImage)) {
|
||||
item := models.HistoryItem{
|
||||
Data: imgOut,
|
||||
DataType: 1, // 1 for image/png
|
||||
Timestamp: time.Now(),
|
||||
Pinned: false,
|
||||
}
|
||||
history.Add(item)
|
||||
lastImage = imgOut
|
||||
log.Info().Msg("Image clipboard item added to history")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
36
src/config/config.go
Normal file
36
src/config/config.go
Normal file
@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Logging struct {
|
||||
Format string `yaml:"format"`
|
||||
Level string `yaml:"level"`
|
||||
} `yaml:"logging"`
|
||||
Clipboard struct {
|
||||
MaxItems int `yaml:"max_items"`
|
||||
} `yaml:"clipboard"`
|
||||
}
|
||||
|
||||
func LoadConfig() (Config, string) {
|
||||
paths := []string{"/etc/kcm/config.yml", "./config.yml"}
|
||||
var cfg Config
|
||||
for _, path := range paths {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
d := yaml.NewDecoder(file)
|
||||
if err := d.Decode(&cfg); err == nil {
|
||||
return cfg, path
|
||||
}
|
||||
}
|
||||
cfg.Logging.Format = "console"
|
||||
cfg.Logging.Level = "info"
|
||||
return cfg, ""
|
||||
}
|
85
src/main.go
Normal file
85
src/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/vleeuwenmenno/kcm/src/commands"
|
||||
"github.com/vleeuwenmenno/kcm/src/config"
|
||||
"github.com/vleeuwenmenno/kcm/src/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, cfgPath := config.LoadConfig()
|
||||
|
||||
// Set log level
|
||||
level, err := zerolog.ParseLevel(cfg.Logging.Level)
|
||||
if err != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
zerolog.SetGlobalLevel(level)
|
||||
|
||||
// Set log format
|
||||
if cfg.Logging.Format == "console" {
|
||||
zerolog.TimeFieldFormat = "[" + time.RFC3339 + "] - "
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||
} else {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
}
|
||||
|
||||
log.Debug().Str("config", cfgPath).Msg("Loaded configuration")
|
||||
|
||||
history := &models.History{MaxItems: cfg.Clipboard.MaxItems}
|
||||
history.Load()
|
||||
|
||||
shouldReturn := handleDebugging()
|
||||
if shouldReturn {
|
||||
return
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kcm",
|
||||
Short: "Clipboard Manager CLI",
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(
|
||||
commands.NewWatchCmd(history),
|
||||
commands.NewListCmd(history),
|
||||
commands.NewClearCmd(history),
|
||||
commands.NewCopyCmd(history),
|
||||
)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal().AnErr("err", err).Msg("Error executing command")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
handleDebugging enables interactive debugging mode.
|
||||
If "debug" is the first argument, prompts for subcommand/args and injects them into os.Args.
|
||||
Returns true if debugging mode was triggered.
|
||||
*/
|
||||
func handleDebugging() bool {
|
||||
if len(os.Args) > 1 && os.Args[1] == "debug" {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
log.Debug().Msg("Enter the subcommand and arguments you want to run: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
command, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatal().AnErr("err", err).Msg("Error reading input")
|
||||
return false
|
||||
}
|
||||
command = strings.TrimSpace(command)
|
||||
os.Args = append(os.Args[:1], strings.Split(command, " ")...)
|
||||
log.Debug().
|
||||
Str("input", command).
|
||||
Msg("Executing command")
|
||||
}
|
||||
return false
|
||||
}
|
107
src/models/history.go
Normal file
107
src/models/history.go
Normal file
@ -0,0 +1,107 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/vleeuwenmenno/kcm/src/utils"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
type History struct {
|
||||
Items []HistoryItem
|
||||
mu sync.Mutex
|
||||
MaxItems int
|
||||
}
|
||||
|
||||
func historyFilePath() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "./history.gob"
|
||||
}
|
||||
dir := filepath.Join(usr.HomeDir, ".local", "share", "kcm")
|
||||
os.MkdirAll(dir, 0700)
|
||||
return filepath.Join(dir, "history.gob")
|
||||
}
|
||||
|
||||
func (h *History) Save() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
file, err := os.Create(historyFilePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
enc := gob.NewEncoder(file)
|
||||
return enc.Encode(h.Items)
|
||||
}
|
||||
|
||||
func (h *History) Load() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
file, err := os.Open(historyFilePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
dec := gob.NewDecoder(file)
|
||||
return dec.Decode(&h.Items)
|
||||
}
|
||||
|
||||
func (h *History) Add(item HistoryItem) {
|
||||
h.mu.Lock()
|
||||
// Prevent duplicates: remove any existing item with the same hash
|
||||
itemHash := utils.HashBytes(item.Data)
|
||||
var newItems []HistoryItem
|
||||
for _, existing := range h.Items {
|
||||
if utils.HashBytes(existing.Data) != itemHash {
|
||||
newItems = append(newItems, existing)
|
||||
} else {
|
||||
log.
|
||||
Info().
|
||||
Str("hash", itemHash).
|
||||
Str("hash_existing", utils.HashBytes(existing.Data)).
|
||||
Str("timestamp", item.Timestamp.Format(time.RFC3339)).
|
||||
Msg("Duplicate detected: replaced previous entry with new timestamp.")
|
||||
}
|
||||
}
|
||||
h.Items = newItems
|
||||
// Add the new item
|
||||
h.Items = append(h.Items, item)
|
||||
if h.MaxItems > 0 && len(h.Items) > h.MaxItems {
|
||||
over := len(h.Items) - h.MaxItems
|
||||
h.Items = h.Items[over:]
|
||||
}
|
||||
h.mu.Unlock()
|
||||
h.Save()
|
||||
}
|
||||
|
||||
func (h *History) Clear() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.Items = nil
|
||||
return h.Save()
|
||||
}
|
||||
|
||||
func (h *History) List(w io.Writer) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for i, item := range h.Items {
|
||||
typeStr := "text"
|
||||
if item.DataType == clipboard.FmtImage {
|
||||
typeStr = "image"
|
||||
}
|
||||
fmt.Fprintf(w, "%d: [%s] %s @ %s\n", i+1, typeStr, utils.Summary(item.Data, item.DataType), item.Timestamp.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *History) ReloadIfChanged() error {
|
||||
return h.Load()
|
||||
}
|
14
src/models/history_item.go
Normal file
14
src/models/history_item.go
Normal file
@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
type HistoryItem struct {
|
||||
Data []byte
|
||||
DataType clipboard.Format
|
||||
Timestamp time.Time
|
||||
Pinned bool
|
||||
}
|
11
src/utils/hash.go
Normal file
11
src/utils/hash.go
Normal file
@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// HashBytes returns a SHA256 hash of the given byte slice as a string.
|
||||
func HashBytes(data []byte) string {
|
||||
h := sha256.Sum256(data)
|
||||
return string(h[:])
|
||||
}
|
20
src/utils/summary.go
Normal file
20
src/utils/summary.go
Normal file
@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
func Summary(data []byte, format clipboard.Format) string {
|
||||
if format == clipboard.FmtText {
|
||||
if len(data) > 40 {
|
||||
return string(data[:40]) + "..."
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
if format == clipboard.FmtImage {
|
||||
return "[image] " + strconv.Itoa(len(data)) + " bytes"
|
||||
}
|
||||
return "[unknown format]"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user