51 Commits
Author SHA1 Message Date
RandyTheSilly ff9ea4640f Prepare for release v0.4.0 2026-02-17 20:49:37 -05:00
RandyTheSilly 8d88f9c7fd Sync with latest libmutton development version 2026-02-14 17:14:48 -05:00
RandyTheSilly b6e53451f1 Fix clipboard not clearing as expected 2026-02-14 17:04:57 -05:00
RandyTheSilly b148dbf8bf Address JetBrains warnings; update documentation + draft release notes 2026-02-14 16:21:04 -05:00
RandyTheSilly 18fb930a94 Fix build on many platforms (via libmutton bump) 2026-02-13 19:16:49 -05:00
RandyTheSilly 70ac2ad8ee Sync with latest libmutton development version 2026-02-13 18:50:02 -05:00
RandyTheSilly d6d9a56532 Sync with latest libmutton development version 2026-01-25 17:12:06 -05:00
RandyTheSilly 966f340ddd Bump libmutton+rcw to add error handling for corrupt entries 2026-01-25 16:30:13 -05:00
RandyTheSilly f4abb4cc7e Do not preserve new lines when rendering Markdown 2026-01-24 18:45:53 -05:00
RandyTheSilly 538d043656 Refactor all of entryList.go for efficiency 2026-01-24 18:45:04 -05:00
RandyTheSilly 926eb1f176 Implement entry verification in tweak menu+after entry refresh; disable CGO across the board 2026-01-24 15:58:45 -05:00
RandyTheSilly 8e59d81fff Sync with latest libmutton development version 2026-01-24 14:10:03 -05:00
RandyTheSilly 1afbce9b1b Sync with latest libmutton development version 2026-01-21 19:12:13 -05:00
RandyTheSilly 8f9aa7eef3 Sync with latest libmutton development version 2026-01-21 19:12:13 -05:00
RandyTheSilly 5f02e41c05 Also capture syscall.SIGINT in copy menu; bump libmutton to fix Windows background processes dying with parent terminal 2026-01-10 23:39:38 -05:00
RandyTheSilly 0211941a48 Actually bump sys 2026-01-08 23:09:01 -05:00
RandyTheSilly 0dfaf20fec Adjust input spacing in many areas for a more consistent look 2026-01-08 23:07:13 -05:00
RandyTheSilly 32364717dc Sync with latest libmutton development version; draft documentation for release v0.4.0 2026-01-08 21:23:36 -05:00
RandyTheSilly 9af456590c Sync with latest libmutton development version 2026-01-08 18:22:54 -05:00
RandyTheSilly 441730e0e0 Sync with latest libmutton development version 2026-01-01 20:03:23 -05:00
RandyTheSilly 25f2013d62 Update documentation 2025-12-26 21:50:27 -05:00
RandyTheSilly cac35e1d25 Fix clipboard clear error when terminating copy menu 2025-12-26 20:50:36 -05:00
RandyTheSilly 88c60cc036 Address JetBrains warnings; bump dependencies 2025-12-26 19:38:21 -05:00
RandyTheSilly aaf69eb590 Misc. optimizations to copy menu 2025-12-26 18:26:56 -05:00
RandyTheSilly 12167b1eed Auto-exit copy menu on timer if no fields are selected 2025-12-26 15:10:26 -05:00
RandyTheSilly 48ecf7cf83 Sync with latest libmutton development version (new config file format) 2025-12-17 00:41:44 -05:00
RandyTheSilly fb70b38dc3 Sync with latest libmutton development version 2025-12-11 20:14:19 -05:00
RandyTheSilly 19dd1fd685 Sync with latest libmutton development version (new server binary compatibility) 2025-12-10 00:14:55 -05:00
RandyTheSilly 7016a8a130 Account for aging dots in entry list line wrapping 2025-11-23 14:58:02 -05:00
RandyTheSilly 8112496006 Replace entry reader with copy menu in most cases 2025-11-23 14:49:49 -05:00
RandyTheSilly b26c2ce17c Move username to top of copy menu; send user to copy menu after creating/editing an entry; allow copying old password from copy menu 2025-11-23 13:40:41 -05:00
RandyTheSilly 23c6d2e354 Add v0.3.1 release notes to main branch 2025-11-23 12:49:42 -05:00
RandyTheSilly 63fb511649 Correct license in PKGBUILD/APKBUILD 2025-11-23 04:40:35 -05:00
RandyTheSilly 4c4f025d5b Bump golang.org/x/crypto to address CVE-2025-58181 and CVE-2025-47914 2025-11-23 03:40:09 -05:00
RandyTheSilly 1a9de90f89 Enable entry password aging (sync with latest libmutton development version) 2025-11-23 03:34:31 -05:00
RandyTheSilly 300c433909 Sync with latest libmutton development version 2025-11-13 23:04:55 -05:00
RandyTheSilly bbd42728d0 Update roadmap 2025-11-11 01:22:30 -05:00
RandyTheSilly 55cd9b99da Rename platform-specific files for more clarity 2025-11-11 01:18:26 -05:00
RandyTheSilly 6478957372 Bump libmutton to fix Windows compilation 2025-11-10 23:19:40 -05:00
RandyTheSilly b58461b9c4 Remove extra trailing newline after TOTP refresher service stop notification 2025-11-10 22:38:44 -05:00
RandyTheSilly a75416a679 Fix copy menu panic on entries without notes 2025-11-10 19:39:51 -05:00
RandyTheSilly fc1e3b11fc Add copy menu option to shell completions 2025-11-10 19:35:40 -05:00
RandyTheSilly 107a6f4f12 Adjust spacing in copy menu 2025-11-10 01:13:35 -05:00
RandyTheSilly 39a3460779 Bump libmutton to latest development version 2025-11-10 01:09:25 -05:00
RandyTheSilly 60d6961ff4 Move some ANSI constants to go-boilerplate; bump libmutton to latest development version 2025-11-10 01:03:59 -05:00
RandyTheSilly 13aa507788 Set beta version; update beta versioning comment 2025-11-10 00:21:12 -05:00
RandyTheSilly c1c9565511 Clear clipboard when copy process is closed 2025-11-10 00:13:49 -05:00
RandyTheSilly 41b4c4b6ca Initial interactive copy menu 2025-11-09 23:35:48 -05:00
RandyTheSilly bc35766de5 Sync with libmutton v0.4.2 2025-11-09 18:15:18 -05:00
RandyTheSilly 3cb0bdce59 Rename all occurrences of "passphrase" to "password"; bump libmutton 2025-10-26 13:25:42 -04:00
RandyTheSilly bfb6372c29 Correct misleading variable name 2025-10-26 12:10:09 -04:00
29 changed files with 553 additions and 417 deletions
+1 -3
View File
@@ -1,8 +1,6 @@
/*.exe*
/mutn
/mutn.exe
/MUTN
/MUTN.exe
/main
/main.exe
/version.go
1output
+1 -1
View File
@@ -1,5 +1,5 @@
MUTN
Copyright (c) 2024-2025 Randall Winkhart
Copyright (c) 2024-2026 Randall Winkhart
MUTN is licensed under the AGPL 3.0 due to its usage of
a custom fork of https://github.com/jessp01/gohighlight.
+2 -8
View File
@@ -2,7 +2,7 @@
---
Pronounced as: "mutton", "muh·tn"
MUTN is a simple, self-hosted, SSH-synchronized password and note manager based on [libmutton](https://github.com/rwinkhart/libmutton). It is the successor to [sshyp](https://github.com/rwinkhart/sshyp).
MUTN is a simple, self-hosted, vertically integrated, SSH-synchronized password manager. It is based on and is the reference implementation of [libmutton](https://github.com/rwinkhart/libmutton).
> [!WARNING]
>It is your responsibility to assess the security and stability of MUTN and to ensure it meets your needs before using it.
@@ -23,14 +23,8 @@ Additionally, MUTN is available as a source package ("[mutn](https://aur.archlin
After installing, please review the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/usage.md).
# Roadmap
### Release v0.4.0
- [ ] Switch to fully compliant Markdown (do not preserve new lines)
- [ ] Switch to alternative Markdown renderer (minimark or BEAN)
### Release v0.5.0
- [ ] libmutton v0.5.0
- [ ] Implement "netpin" (quick-unlock)
- [ ] Password aging support
- [ ] Add yellow/red dot indicators to entry list readout for when passwords should be changed
- Switch to alternative Markdown renderer (minimark or BEAN)
### Release v1.0.0 - Distribution packages
- [ ] Create packaging scripts
- [x] Stable source PKGBUILD
+1 -1
View File
@@ -28,7 +28,7 @@ _mutnCompletions() {
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password note folder" -- "$cur" )
;;
copy )
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note" -- "$cur" )
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note menu" -- "$cur" )
;;
edit )
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note rename" -- "$cur" )
+1 -1
View File
@@ -86,7 +86,7 @@ function cliMUTN-optionCompleter {
$possibleValues = @{
add = @('password', 'note', 'folder')
copy = @('password', 'username', 'totp', 'url', 'note')
copy = @('password', 'username', 'totp', 'url', 'note', 'menu')
edit = @('password', 'username', 'totp', 'url', 'note', 'rename')
gen = @('update')
}
+1 -1
View File
@@ -20,7 +20,7 @@ case ${words[-2]} in
compadd {password,note,folder}
;;
copy )
compadd {password,username,totp,url,note}
compadd {password,username,totp,url,note,menu}
;;
edit )
compadd {password,username,totp,url,note,rename}
+7 -5
View File
@@ -1,7 +1,7 @@
.TH MUTN 1 "17 August 2025" "v0.3.0" "MUTN man page"
.TH MUTN 1 "17 February 2026" "v0.4.0" "MUTN man page"
.SH NAME
\fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton. It is the successor to sshyp.
\fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton.
.SH SYNOPSIS
Usage: mutn [/<entry name> [argument] [option]] | [argument]
@@ -63,6 +63,7 @@ copy:
totp|-t Generate and copy the TOTP token for an entry to your clipboard
url|-l Copy the URL in an entry to your clipboard
note|-n Copy the first note line in an entry to your clipboard
menu|-m Open interactive menu to copy any combination of fields
edit:
password|-pw Change the password in an entry
@@ -83,11 +84,11 @@ add:
.SH TIPS
1. You can quickly read an entry with "mutn /<entry name>"
2. Type "mutn" (no arguments/options) to view a list of saved entries
2. Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
3. Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
3. Various changes can be made to the configuration and stored entries from the "mutn tweak" menu
4. Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
4. Type "mutn" (no arguments) to view a list of saved entries w/colored markers indicating the ages of stored passwords
.SH SETUP
libmutton operates on a client-server model, and thus sync support requires you to have access to your own server (whether it be a physical home server or a cloud rental) with remote access via SSH.
@@ -101,6 +102,7 @@ Once this has been done, you may proceed with the client setup:
2. Run "mutn init" and follow the prompts
3. If you already have entries on the server, sync them using "mutn sync"
4. Optionally, shell completions (Bash, ZSH, and PowerShell 7+) can be enabled with your shell's respective method
5. If importing passwords from another password manager, you may desire to use the "Age all entries" option from the "mutn tweak" menu to add initial age tracking data to the imported passwords
.SH TROUBLESHOOTING
.B Clipboard not clearing:
+10
View File
@@ -1,5 +1,15 @@
**WARNING: AS LIBMUTTON HAS NOT YET REACHED v1.0.0, [BREAKING CHANGES](https://github.com/rwinkhart/libmutton/blob/main/wiki/breaking.md) IN FUTURE UPDATES ARE PLANNED**
**MUTN v0.3.1**
Built with libmutton v0.4.4
November 23, 2025
This is a simple dependency bump and bugfix release. Security issues are addressed. See the [libmutton v0.4.4 patchnotes](https://github.com/rwinkhart/libmutton/releases/tag/v0.4.4) for more information.
A new major version is well underway. This is likely the last release in the v0.3.x series before more breaking changes are made.
---
**MUTN v0.3.0**
Built with libmutton v0.4.0
August 17, 2025
+31
View File
@@ -0,0 +1,31 @@
**WARNING: AS LIBMUTTON HAS NOT YET REACHED v1.0.0, [BREAKING CHANGES](https://github.com/rwinkhart/libmutton/blob/main/wiki/breaking.md) IN FUTURE UPDATES ARE PLANNED**
**MUTN v0.4.0**
Built with libmutton v0.5.0
February 17, 2026
# The "Hardy Herd" Update
This release adds password age-tracking support, a new interactive copy menu, and features many under-the-hood (breaking) changes.
## libmutton-derived Changes
- **PLEASE** read [libmutton's release notes](https://github.com/rwinkhart/libmutton/releases/tag/v0.5.0), as they detail severe breaking changes. You **NEED** to update your `libmuttonserver` and run `mutn init` after this update!
## Features
- (41b4c4b6ca2f5a74899e87c6d520df7b140671b3) (c1c95655114a689daf80d2410e19ba35b670b1b1) (107a6f4f1250de8f94d87c192d58cc47bef80c1f) (fc1e3b11fc810a79cc0673b6edf96d4753c0300c) (a75416a6792559179e39497cb033407f18c00620) (b58461b9c458795278f050ff10e91a2d05c21745) (b26c2ce17c0491890de03dc131717d557876a3e4) (8112496006c7d48dd4a8f21a9eea51eadc30f732) (12167b1eeddb49e8f5e62f788dc1da6265706f68) (aaf69eb590af4dc37013102dcff79ef115b9f809) (cac35e1d25b7a9a4bd7263406f642834418f6b76) Add new interactive copy menu
- This menu aims to reduce the number of times the user is decrypting each entry
- (1a9de90f890f133e6ab8f298241fe3a6237ba4c8) (7016a8a1309b06c63b9dd63fb126103d4cab6ccc) Password age tracking (from libmutton)
- The `tweak` menu now allows adding age data to old (existing) entries
- (926eb1f1766f28a69e194c74758c300f233856fb) Option to verify entries (integrity) in `tweak` menu
## Changes
- (f4abb4cc7e5f303f68358fcdda659056422ebeb3) MUTN no longer preserves new lines in Markdown notes (more compliant)
## Dependencies (direct/replaced)
- Dropped (libmutton)
- golang.design/x/clipboard
- gopkg.in/ini.v1
- Bumped
- Go: v1.25.4 => 1.26.0
- github.com/rwinkhart/go-boilerplate: v0.1.0 => v0.3.1
- github.com/rwinkhart/libmutton: v0.4.4 => v0.5.0
- golang.org/x/term: v0.36.0 => v0.40.0
+16 -21
View File
@@ -1,12 +1,12 @@
module github.com/rwinkhart/MUTN
go 1.24.6
go 1.26.0
require (
github.com/charmbracelet/glamour v0.7.0
github.com/rwinkhart/go-boilerplate v0.1.0
github.com/rwinkhart/libmutton v0.4.0
golang.org/x/term v0.34.0
github.com/rwinkhart/go-boilerplate v0.3.1
github.com/rwinkhart/libmutton v0.5.0
golang.org/x/term v0.40.0
)
require (
@@ -14,37 +14,32 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/sftp v1.13.9 // indirect
github.com/pkg/sftp v1.13.10 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rwinkhart/go-highlite v0.1.1 // indirect
github.com/rwinkhart/peercred-mini v0.1.1 // indirect
github.com/rwinkhart/rcw v0.2.2 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/rwinkhart/peercred-mini v0.1.4 // indirect
github.com/rwinkhart/rcw v0.3.0 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.design/x/clipboard v0.7.1 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/image v0.30.0 // indirect
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace golang.org/x/sys => github.com/rwinkhart/sys v0.35.0
replace golang.org/x/sys => github.com/rwinkhart/sys v0.41.0
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.1
replace github.com/charmbracelet/glamour => github.com/rwinkhart/glamour-temp-MUTN v0.7.2
+30 -95
View File
@@ -5,22 +5,23 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -29,8 +30,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -41,103 +42,37 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rwinkhart/glamour-temp-MUTN v0.7.2 h1:/CBexwkGdL8HuZjO7M0zzjGHaHCr9/r4XTOtUJa+pPE=
github.com/rwinkhart/glamour-temp-MUTN v0.7.2/go.mod h1:DN/eJaNQ5h7dgxzfmWFo7By+b82CKEUdNaY6z4R4v3c=
github.com/rwinkhart/go-boilerplate v0.1.0 h1:EzlVj6R7Bxtl79Nl7R5zRcg6s+Cf2FAqGIzR4giWTQg=
github.com/rwinkhart/go-boilerplate v0.1.0/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
github.com/rwinkhart/go-boilerplate v0.3.1 h1:vkVRuptO2s1yPzzwpvtiiB0c/hPB6yr0p1mzZ2Myb9E=
github.com/rwinkhart/go-boilerplate v0.3.1/go.mod h1:ES13A2r9fnCVfyezwMBgY/RgA4pOIudOUXz3Jk/ikes=
github.com/rwinkhart/go-highlite v0.1.1 h1:9TxbRhYVfD/3YaEgNk1BtEiZ0t8/5mxDUZqNMXHodT0=
github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/rwinkhart/libmutton v0.4.0 h1:dUB192cYaYWgfMzgLrM+eq3zCtKXNsXq7l8Kcfoe4RE=
github.com/rwinkhart/libmutton v0.4.0/go.mod h1:QLDVXlwnQsySGew0YG6z3AbQBO8CFvm3cIw3WvA4Crs=
github.com/rwinkhart/peercred-mini v0.1.1 h1:hDoqSEwynJN4w8OWlZNSQAjAum/ocP3AYF7DjtPV5qU=
github.com/rwinkhart/peercred-mini v0.1.1/go.mod h1:dstv+IydIklCnffwAZgD2AWqkGe0mETn3lLtqJjYAeI=
github.com/rwinkhart/rcw v0.2.2 h1:bTUi3BLjrcoibi5YDlzeiVKo608EmGetymXbJVM3oYE=
github.com/rwinkhart/rcw v0.2.2/go.mod h1:lhTErVEG3klKVJkcoHuTw5pDHiS3Irlj7Z03IshHh7M=
github.com/rwinkhart/sys v0.35.0 h1:6ZikETjyuv9ndnSEnXQmp/M3lxyCHWoHAm3lGzy0pnE=
github.com/rwinkhart/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
github.com/rwinkhart/go-winio v0.1.1 h1:kAJKiqneR7cUR01Wn5/doAAV4kOGTEGPug4oinXc5N4=
github.com/rwinkhart/go-winio v0.1.1/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/rwinkhart/libmutton v0.5.0 h1:Du/o5Hyb26uE6lubqBISN/ZF9JZ5oObHVveQjAqrP0E=
github.com/rwinkhart/libmutton v0.5.0/go.mod h1:peh8A1f3gnaF97m8J06U28JloKv04Kw8WtFCH+Pcq6s=
github.com/rwinkhart/peercred-mini v0.1.4 h1:93+phjLknvJadEd2cu/ZPPWdfRSPOwFJzDBEn4ZtWVc=
github.com/rwinkhart/peercred-mini v0.1.4/go.mod h1:E8eApo/izzmq4nGGR+kpJ0ZLAXjofnVRMGCeVMPQ/Ik=
github.com/rwinkhart/rcw v0.3.0 h1:OdgA++IGcDrZqN6FzyYnzbz04fL8Q1MHvlgPjhtgJM4=
github.com/rwinkhart/rcw v0.3.0/go.mod h1:UUY7kSku6kV2X1RWPUcK0Re+rQ6RvngWI8DVZJ11cqk=
github.com/rwinkhart/sys v0.41.0 h1:pHB6HphVC132UXYZ6yeOk62abqgl+pyl5ZZnsk4iUKg=
github.com/rwinkhart/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
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.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6 h1:UmlTtCyqnD3jxsGe973Y9uZydewwYUboXPux+RCcKQU=
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6/go.mod h1:QnFR+evpZFrYgSiu+d/Rn6g/6bNqLQTp+rzKaVpFoeI=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9 h1:tf0OY/FXi1sPkoNVKP4w+GStqIfqbFUqDoDDm4B+iCg=
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9/go.mod h1:wNiuiJfmmgv45sw8EHpNeVWqpxLeEwQ8bkUIhuOYUh8=
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/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+83 -59
View File
@@ -11,12 +11,13 @@ import (
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/cfg"
"github.com/rwinkhart/libmutton/age"
"github.com/rwinkhart/libmutton/clip"
"github.com/rwinkhart/libmutton/config"
"github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/crypt"
"github.com/rwinkhart/libmutton/global"
"github.com/rwinkhart/libmutton/syncclient"
"github.com/rwinkhart/libmutton/synccycles"
)
func main() {
@@ -33,33 +34,31 @@ func main() {
// if the first argument is an entry...
if strings.HasPrefix(args[1], "/") {
// store location of target entry
targetLocation := global.TargetLocationFormat(args[1])
// store realPath of target entry
realPath := global.GetRealPath(args[1])
// entry reader shortcut (if no other arguments are supplied)
if argsCount == 2 {
cli.EntryReaderDecrypt(targetLocation, true)
cli.EntryReaderDecrypt(realPath, true)
// perform other operations on the entry (if other arguments are supplied)
} else if argsCount == 3 || (argsCount == 4 && (args[3] == "show" || args[3] == "-s")) {
if argsCount == 3 { // default to "password" if no field is specified (for copy, edit, and add)
switch args[2] {
case "show", "-s":
cli.EntryReaderDecrypt(targetLocation, false)
cli.EntryReaderDecrypt(realPath, false)
case "copy":
err := core.CopyArgument(targetLocation, 0)
if err != nil {
other.PrintError("Failed to copy passphrase to clipboard: "+err.Error(), global.ErrorClipboard)
if err := clip.CopyShortcut(realPath, 0, nil); err != nil {
other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard)
}
case "edit":
cli.EditEntryField(targetLocation, true, 0)
cli.EditEntryField(realPath, 0)
case "gen":
cli.AddEntry(targetLocation, true, 1)
cli.AddEntry(realPath, 1)
case "add":
cli.AddEntry(targetLocation, true, 0)
cli.AddEntry(realPath, 0)
case "shear":
err := syncclient.ShearRemoteFromClient(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
if err != nil {
if err := syncclient.ShearRemote(args[1], false); err != nil {
other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite)
}
default:
@@ -68,11 +67,11 @@ func main() {
} else { // handle "show" or "-s" argument for gen, edit, and add
switch args[2] {
case "edit":
cli.EditEntryField(targetLocation, false, 0)
cli.EditEntryField(realPath, 0)
case "gen":
cli.AddEntry(targetLocation, false, 1)
cli.AddEntry(realPath, 1)
case "add":
cli.AddEntry(targetLocation, false, 0)
cli.AddEntry(realPath, 0)
default:
cli.HelpMain()
}
@@ -94,11 +93,12 @@ func main() {
field = 3
case "note", "-n":
field = 4
case "menu", "-m":
cli.CopyMenu(args[1], nil, nil)
default:
cli.HelpCopy()
}
err := core.CopyArgument(targetLocation, field)
if err != nil {
if err := clip.CopyShortcut(realPath, field, nil); err != nil {
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
}
case "edit":
@@ -115,38 +115,38 @@ func main() {
case "note", "-n":
field = 4
case "rename", "-r":
isAccessible, _ := back.TargetIsFile(targetLocation, true) // error is ignored because dir/file status is irrelevant
isAccessible, _ := back.TargetIsFile(realPath, true) // error is ignored because dir/file status is irrelevant
if !isAccessible {
other.PrintError("Failed to access location ("+targetLocation+")", back.ErrorTargetNotFound)
other.PrintError("Failed to access location ("+realPath+")", back.ErrorTargetNotFound)
}
cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
default:
cli.HelpEdit()
}
if argsCount == 4 {
cli.EditEntryField(targetLocation, true, field)
cli.EditEntryField(realPath, field)
} else {
switch args[4] {
case "show", "-s":
cli.EditEntryField(targetLocation, false, field)
cli.EditEntryField(realPath, field)
default:
cli.EditEntryField(targetLocation, true, field)
cli.EditEntryField(realPath, field)
}
}
case "gen":
if argsCount == 4 {
switch args[3] {
case "update", "-u":
cli.GenUpdate(targetLocation, true)
cli.GenUpdate(realPath)
default:
cli.HelpGen()
}
} else if args[3] == "update" || args[3] == "-u" {
switch args[4] {
case "show", "-s":
cli.GenUpdate(targetLocation, false)
cli.GenUpdate(realPath)
default:
cli.GenUpdate(targetLocation, true)
cli.GenUpdate(realPath)
}
}
cli.HelpGen()
@@ -154,20 +154,19 @@ func main() {
switch args[3] {
case "password", "-pw":
if argsCount == 4 {
cli.AddEntry(targetLocation, true, 0)
cli.AddEntry(realPath, 0)
} else {
switch args[4] {
case "show", "-s":
cli.AddEntry(targetLocation, false, 0)
cli.AddEntry(realPath, 0)
default:
cli.AddEntry(targetLocation, true, 0)
cli.AddEntry(realPath, 0)
}
}
case "note", "-n":
cli.AddEntry(targetLocation, true, 2)
cli.AddEntry(realPath, 2)
case "folder", "-f":
err := syncclient.AddFolderRemoteFromClient(args[1]) // pass the incomplete path as the server will have a different home directory
if err != nil {
if err := syncclient.AddFolderRemote(args[1]); err != nil {
other.PrintError("Failed to add folder: "+err.Error(), back.ErrorWrite)
}
default:
@@ -182,50 +181,65 @@ func main() {
} else {
switch args[1] {
case "clipclear":
err := core.ClipClearArgument()
if err != nil {
if err := clip.ClearArgument(); err != nil {
other.PrintError("Failure occurred in clipboard clearing process: "+err.Error(), global.ErrorClipboard)
}
case "startrcwd":
crypt.RCWDArgument()
case "sync":
_, err := syncclient.RunJob(true)
_, err := syncclient.RunJob()
if err != nil {
other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess)
}
case "init":
err := core.LibmuttonInit(front.Input,
[][3]string{{"MUTN", "textEditor", getTextEditorInput()}},
confirmRCWPassphrase("new"), false)
err := core.LibmuttonInit(
front.Input,
confirmRCWPassword("new"),
front.InputBinary("Preserve existing config directory?"),
false,
"",
map[string]any{"mutnTextEditor": getTextEditorInput()},
)
if err != nil {
other.PrintError("Initialization failed: "+err.Error(), 0)
}
case "tweak":
choice := front.InputMenuGen("Action:", []string{"Change device ID", "Change master passphrase/Optimize entries", "Set text editor"})
choice := front.InputMenuGen("Action:", []string{"Set text editor", "Change device ID", "Age all entries", "Verify all entries", "Change master password/Optimize entries"})
switch choice {
case 1:
newCfg := &config.CfgT{}
newClientSpecificCfg := map[string]any{"mutnTextEditor": getTextEditorInput()}
newCfg.ClientSpecific = &newClientSpecificCfg
if err := config.Write(newCfg, true); err != nil {
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
}
case 2:
oldDeviceID, err := global.GetCurrentDeviceID()
if err != nil {
other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead)
}
_, _, err = synccycles.DeviceIDGen(oldDeviceID, "")
_, _, _, err = syncclient.GenDeviceID(oldDeviceID, "")
if err != nil {
other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess)
}
fmt.Println("\nDevice ID changed successfully.")
case 2:
oldPassphrase := confirmRCWPassphrase("old")
newPassphrase := confirmRCWPassphrase("new")
fmt.Print("\nRe-encrypting entries. Please wait; do not force close this process.\n")
err := core.EntryRefresh(oldPassphrase, newPassphrase, false)
if err != nil {
case 3:
forceReage := front.InputBinary("Re-age aged entries?")
fmt.Println("Aging entries; please wait - do not terminate this process")
if err := age.AllPasswordEntries(forceReage, nil); err != nil {
other.PrintError("Failed to age entries: "+err.Error(), 1)
}
case 4:
verifyEntries(front.InputSecret("RCW Password:"))
case 5:
oldPassword := confirmRCWPassword("old")
newPassword := confirmRCWPassword("new")
fmt.Print("Re-encrypting entries; please wait - do not terminate this process.\n\n")
if err := core.EntryRefresh(oldPassword, newPassword, false); err != nil {
other.PrintError("Re-encryption failed: "+err.Error(), global.ErrorEncryption)
}
fmt.Println("\nRe-encryption complete.")
case 3:
err := cfg.WriteConfig([][3]string{{"MUTN", "textEditor", getTextEditorInput()}}, nil, true)
if err != nil {
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
if front.InputBinary("\nRe-encryption complete. Verify new entries (recommended)?") {
verifyEntries(newPassword)
}
}
case "copy":
@@ -245,14 +259,14 @@ func main() {
}
}
// confirmRCWPassphrase prompts the user for a master passphrase and confirms it
// is typed correctly. Returns the passphrase as a byte slice.
func confirmRCWPassphrase(lowercasePrefix string) []byte {
// confirmRCWPassword prompts the user for a master password and confirms it
// is typed correctly. Returns the password as a byte slice.
func confirmRCWPassword(lowercasePrefix string) []byte {
for {
// get master passphrase
rcwPass := front.InputHidden(strings.ToUpper(string(lowercasePrefix[0])) + lowercasePrefix[1:] + " master passphrase:")
if !bytes.Equal(rcwPass, front.InputHidden("Confirm "+lowercasePrefix+" master passphrase:")) || len(rcwPass) == 0 {
fmt.Println(back.AnsiError + "Passphrases do not match or passphrase is invalid" + back.AnsiReset)
// get master password
rcwPass := front.InputSecret(strings.ToUpper(string(lowercasePrefix[0])) + lowercasePrefix[1:] + " master password:")
if !bytes.Equal(rcwPass, front.InputSecret("Confirm "+lowercasePrefix+" master password:")) || len(rcwPass) == 0 {
fmt.Println(back.AnsiError + "Passwords do not match or password is invalid" + back.AnsiReset)
continue
}
return rcwPass
@@ -263,3 +277,13 @@ func confirmRCWPassphrase(lowercasePrefix string) []byte {
func getTextEditorInput() string {
return cmp.Or(front.Input("Text editor (leave blank for $EDITOR, falls back to \""+cli.FallbackEditor+"\"):"), os.Getenv("EDITOR"), cli.FallbackEditor)
}
// verifyEntries attempts to verify all entries using the provided RCW password.
func verifyEntries(rcwPassword []byte) {
fmt.Println("Verification in progress; please wait - do not terminate this process...")
err := core.VerifyEntries(rcwPassword)
if err != nil {
other.PrintError("Verification failed: "+err.Error(), global.ErrorDecryption)
}
fmt.Println("Verification successful")
}
+1 -1
View File
@@ -3,7 +3,7 @@
# get version number from cli package
cd ..
printf 'package main\nimport (\n"fmt"\n"github.com/rwinkhart/MUTN/src/cli"\n)\nfunc main() {\nfmt.Println(cli.MUTNVersion)\n}' > ./version.go
version=$(go run -ldflags="-s -w" -tags noMarkdown ./version.go)
version=$(go run -ldflags="-s -w" ./version.go)
rm ./version.go
cd ./packaging
@@ -11,7 +11,7 @@ pkgrel="$((revision-1))"
pkgdesc='A simple, self-hosted, SSH-synchronized password/note manager for the CLI (based on libmutton)'
arch='all'
url='https://github.com/rwinkhart/MUTN'
license='MIT'
license='AGPL-3.0-only'
depends=''
makedepends='busybox go'
options='!check net'
@@ -41,7 +41,7 @@ build() {
esac
# compile binary
CGO_ENABLED=1 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
CGO_ENABLED=0 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
}
package() {
@@ -10,7 +10,7 @@ pkgrel="$revision"
pkgdesc='A simple, self-hosted, SSH-synchronized password/note manager for the CLI (based on libmutton)'
arch=('x86_64' 'i686' 'i486' 'pentium4' 'aarch64' 'armv7h' 'riscv64')
url='https://github.com/rwinkhart/MUTN'
license=('MIT')
license=('AGPL-3.0-only')
depends=()
makedepends=(go grep gzip)
optdepends=(
@@ -45,7 +45,7 @@ build() {
esac
# compile binary
CGO_ENABLED=1 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
CGO_ENABLED=0 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
}
package() {
+3 -3
View File
@@ -10,9 +10,9 @@ create_release_binaries() {
printf '\nGenerating release binaries...\n'
cd ..
GOOS=linux CGO_ENABLED=0 GOAMD64=v2 go build -o ./packaging/1output/mutn-linux-x86_64_v2 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=linux GOARCH=arm64 GOARM64=v8.0 CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.0 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=linux GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.7+crypto -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=linux CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.0 CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.0 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=linux CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.7+crypto -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=windows CGO_ENABLED=0 GOAMD64=v2 go build -o ./packaging/1output/mutn-windows-x86_64_v2.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=windows GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-windows-arm64v8.7+crypto.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
GOOS=windows CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-windows-arm64v8.7+crypto.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
cd ./packaging
}
+2 -2
View File
@@ -13,11 +13,11 @@ var (
)
const (
MUTNVersion = "0.3.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
MUTNVersion = "0.4.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.B.0" -> "0.2.0", "0.2.A" -> "0.2.1"
ansiBlackOnWhite = "\033[38;5;0;48;5;15m"
)
func init() {
global.GetPassphrase = front.InputHidden
global.GetPassword = front.InputSecret
}
+16 -12
View File
@@ -7,41 +7,45 @@ import (
"github.com/rwinkhart/libmutton/core"
)
// AddEntry creates a new entry at targetLocation by taking user input via CLI prompts.
// AddEntry creates a new entry at realPath by taking user input via CLI prompts.
// Requires: entryType (0 = standard password entry, 1 = auto-generated password entry, 2 = note-only entry).
func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
// ensure targetLocation is valid
_, err := core.EntryAddPrecheck(targetLocation)
func AddEntry(realPath string, entryType uint8) {
// ensure realPath is valid
_, err := core.EntryAddPrecheck(realPath)
if err != nil {
other.PrintError("Failed to add entry: "+err.Error(), back.ErrorWrite)
}
var decryptedEntry []string
var decSlice []string
var password []byte
if entryType < 2 {
username := front.Input("Username:")
// determine whether to generate the password
var password string
if entryType == 0 {
password = string(front.InputHidden("Password:"))
password = front.InputSecret("Password:")
} else {
password = inputPasswordGen()
}
totp := string(front.InputHidden("TOTP secret:"))
totp := string(front.InputSecret("TOTP secret:"))
url := front.Input("URL:")
if front.InputBinary("Add a note to this entry?") {
note, _ := editNote([]string{})
decryptedEntry = append([]string{password, username, totp, url}, note...)
decSlice = append([]string{string(password), username, totp, url}, note...)
} else {
decryptedEntry = []string{password, username, totp, url}
decSlice = []string{string(password), username, totp, url}
}
} else {
note, _ := editNote([]string{})
decryptedEntry = append([]string{"", "", "", ""}, note...)
decSlice = append([]string{"", "", "", ""}, note...)
}
// write and preview the new entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
if password != nil {
writeEntryCLI(realPath, decSlice, true, nil)
} else {
writeEntryCLI(realPath, decSlice, false, nil)
}
}
+138
View File
@@ -0,0 +1,138 @@
package cli
import (
"fmt"
"os"
"os/signal"
"slices"
"syscall"
"time"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/clip"
"github.com/rwinkhart/libmutton/crypt"
"github.com/rwinkhart/libmutton/global"
"github.com/rwinkhart/libmutton/syncclient"
)
// CopyMenu decrypts an entry and allows the user to interactively copy
// fields without having to re-decrypt each time. decSlice can be left nil
// to decrypt the entry specified by vanityPath. If the entry was provided
// decrypted, CopyMenu assumes it was edited and triggers a sync.
func CopyMenu(vanityPath string, decSlice []string, oldPassword []byte) {
realPath := global.GetRealPath(vanityPath)
var err error
if decSlice == nil {
// decrypt entry
decSlice, err = crypt.DecryptFileToSlice(realPath, nil)
if err != nil {
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
}
} else {
fmt.Print("\n")
_, err := syncclient.RunJob()
if err != nil {
other.PrintError("Failed to sync on copy menu entry: "+err.Error(), global.ErrorSyncProcess)
}
}
// determine populated fields in entry
var fieldStrings = []string{"Username", "Password", "TOTP Code", "URL", "Note (first line)"}
var indices = []int{1, 0, 2, 3, 4}
var fieldOptions []string
for i := range indices {
if len(decSlice) > indices[i] && decSlice[indices[i]] != "" {
fieldOptions = append(fieldOptions, fieldStrings[i])
if indices[i] == 0 && oldPassword != nil {
fieldOptions = append(fieldOptions, "Old Password")
}
}
}
// if notes are included, preview them
if len(decSlice) > 4 {
EntryReader(vanityPath, append([]string{"", "", "", ""}, decSlice[4:]...), true)
if len(fieldOptions) < 1 { // if notes are the only thing included, exit
fmt.Printf("\r%sNo copyable fields present, exiting copy menu...%s\n", back.AnsiWarning, back.AnsiReset)
os.Exit(0)
}
}
// set up signal handling for ctrl+c to clear clipboard
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
if err = clip.ClearProcess(nil); err != nil {
other.PrintError("Failed to clear clipboard on exit: "+err.Error(), global.ErrorClipboard)
}
os.Exit(0)
}()
// set up timer to close copy menu if an item is
// not selected (useful to avoid extra user input
// when the copy menu is displayed automatically
// and is not desired)
const timeoutSeconds uint8 = 5
selectedChan := make(chan bool, 1)
go func() {
for i := uint8(1); i <= timeoutSeconds; i++ {
select {
case <-selectedChan:
return
case <-time.After(1 * time.Second):
if i == timeoutSeconds {
fmt.Printf("\r%sNo field selected, exiting copy menu...%s\n", back.AnsiWarning, back.AnsiReset)
os.Exit(0)
}
fmt.Printf("\rField to copy (exiting in %d seconds): ", timeoutSeconds-i)
}
}
}()
// copy selected field to clipboard
var choice int
var selectedField string
for {
fmt.Println()
if selectedField != "" {
choice = front.InputMenuGen("Field to copy:", fieldOptions)
} else {
choice = front.InputMenuGen("Field to copy (exiting in 5 seconds):", fieldOptions)
selectedChan <- true
}
selectedField = fieldOptions[choice-1]
if selectedField == "Old Password" {
if err = clip.CopyBytes(false, oldPassword); err != nil {
other.PrintError("Failed to copy old password to clipboard: "+err.Error(), global.ErrorClipboard)
}
continue
}
choice = indices[slices.Index(fieldStrings, selectedField)]
if choice == 2 {
fmt.Println(back.AnsiWarning + "[Starting]" + back.AnsiReset + " TOTP clipboard refresher")
errorChan := make(chan error, 1)
done := make(chan bool)
go clip.TOTPCopier(decSlice[2], errorChan, done)
// block until first successful copy
if err = <-errorChan; err != nil {
other.PrintError("Failed to copy TOTP token: "+err.Error(), global.ErrorClipboard)
}
// block until the user sends "q"
for {
input := front.Input("Service will run until 'q' is entered:")
if input == "q" {
break
}
}
close(done)
fmt.Println(back.AnsiBlue + "[Stopped]" + back.AnsiReset + " TOTP clipboard refresher")
} else {
if err = clip.CopyBytes(false, []byte(decSlice[choice])); err != nil {
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
}
}
}
}
+38 -35
View File
@@ -9,71 +9,77 @@ import (
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/cfg"
"github.com/rwinkhart/libmutton/config"
"github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/syncclient"
)
// RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server.
func RenameCli(oldLocationIncomplete string) {
func RenameCli(oldVanityPath string) {
// prompt user for new location and rename
newLocationIncomplete := front.Input("New location:")
err := syncclient.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete)
if err != nil {
newVanityPath := front.Input("New location:")
if err := syncclient.RenameRemote(oldVanityPath, newVanityPath); err != nil {
other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite)
}
// exit is done from sync.RenameRemoteFromClient
// exit is done from sync.RenameRemote
}
// EditEntryField edits a field of an entry at targetLocation (user input).
func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// EditEntryField edits a field of an entry at realPath (user input).
func EditEntryField(realPath string, field int) {
// fetch old entry data (with all required lines present)
decryptedEntry, err := core.GetOldEntryData(targetLocation, field)
decSlice, err := core.GetOldEntryData(realPath, field, nil)
if err != nil {
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
}
// edit the field
var oldPassword []byte
switch field {
case 0:
decryptedEntry[field] = string(front.InputHidden("Password:"))
oldPassword = []byte(decSlice[field])
decSlice[field] = string(front.InputSecret("Password:"))
case 1:
decryptedEntry[field] = front.Input("Username:")
decSlice[field] = front.Input("Username:")
case 2:
decryptedEntry[field] = string(front.InputHidden("TOTP secret:"))
decSlice[field] = string(front.InputSecret("TOTP secret:"))
case 3:
decryptedEntry[field] = front.Input("URL:")
decSlice[field] = front.Input("URL:")
case 4: // edit notes fields
// store note and non-note data separately
fieldsMain := decryptedEntry[:4]
fieldsNote := decryptedEntry[4:]
fieldsMain := decSlice[:4]
fieldsNote := decSlice[4:]
// edit the note
editedNote, noteEdited := editNote(fieldsNote)
if !noteEdited { // exit early if the note was not edited
other.PrintError("Entry is unchanged", 0)
}
decryptedEntry = append(fieldsMain, editedNote...)
decSlice = append(fieldsMain, editedNote...)
}
// write and preview the modified entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
if field == 0 {
writeEntryCLI(realPath, decSlice, true, oldPassword)
} else {
writeEntryCLI(realPath, decSlice, false, oldPassword)
}
}
// GenUpdate generates a new password for an entry at targetLocation (user input).
func GenUpdate(targetLocation string, hideSecrets bool) {
// GenUpdate generates a new password for an entry at realPath (user input).
func GenUpdate(realPath string) {
// fetch old entry data
decryptedEntry, err := core.GetOldEntryData(targetLocation, 0)
decSlice, err := core.GetOldEntryData(realPath, 0, nil)
if err != nil {
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
}
// generate a new password
decryptedEntry[0] = inputPasswordGen()
oldPassword := []byte(decSlice[0])
decSlice[0] = string(inputPasswordGen())
// write and preview the modified entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
writeEntryCLI(realPath, decSlice, true, oldPassword)
}
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty).
@@ -88,11 +94,16 @@ func editNote(baseNote []string) ([]string, bool) {
}(tempFile.Name())
// fetch the user's text editor
editorCfg, err := cfg.ParseConfig([][2]string{{"MUTN", "textEditor"}})
cfg, err := config.Load()
if err != nil {
other.PrintError("Failed to retrieve text editor from libmutton.ini\n\nPlease specify one with \"mutn tweak\"", back.ErrorRead)
other.PrintError("Failed to load libmuttoncfg.json: "+err.Error(), back.ErrorRead)
}
var editor string
if value, exists := (*cfg.ClientSpecific)["mutnTextEditor"]; !exists {
other.PrintError("Failed to retrieve text editor from libmuttoncfg.json\n\nPlease specify one with \"mutn tweak\"", back.ErrorRead)
} else {
editor = value.(string)
}
editor := editorCfg[0]
// write baseNote to tempFile (if it is not empty)
baseNoteLen := len(baseNote)
@@ -119,8 +130,7 @@ func editNote(baseNote []string) ([]string, bool) {
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
if err = cmd.Run(); err != nil {
panic(back.AnsiError + "Failed to write note with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
}
@@ -140,12 +150,6 @@ func editNote(baseNote []string) ([]string, bool) {
// close tempFile
_ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed
// remove trailing empty strings from the edited note
note = back.RemoveTrailingEmptyStrings(note)
// clamp trailing whitespace in each note line
core.ClampTrailingWhitespace(note)
// return the edited note if:
// it is different from baseNote AND
// [it is not empty (prevent needless writes when adding blank notes) OR
@@ -153,7 +157,6 @@ func editNote(baseNote []string) ([]string, bool) {
noteLen := len(note)
if !reflect.DeepEqual(note, baseNote) && (noteLen > 0 || baseNoteLen != noteLen) {
return note, true
} else {
return nil, false
}
return nil, false
}
+128 -107
View File
@@ -3,158 +3,179 @@ package cli
import (
"fmt"
"os"
"sort"
"strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/age"
"github.com/rwinkhart/libmutton/synccommon"
)
// ANSI color constants used only in this file
const (
ansiAlternateEntryColor = "\033[38;5;8m"
ansiDirectoryHeader = "\033[38;5;7;48;5;8m"
ansiEmptyDirectoryWarning = "\033[38;5;3m"
ansiAlternateEntryColor = "\033[38;5;8m"
ansiDirectoryHeader = "\033[38;5;7;48;5;8m"
)
// determineIndentation calculates and returns the final visual indentation multiplier (needed to adjust indentation for skipped parent directories); also subtracts "old" text from directory header.
func writeIndent(sb *strings.Builder, indent int) { sb.WriteString(strings.Repeat(" ", indent*2)) }
// determineIndentation calculates the final visual indentation multiplier (adjusts for skipped parents)
// and trims the parent directory name from the directory header (if applicable).
func determineIndentation(skippedDirList []bool, dirList []string, currentDirIndex int) (int, string) {
var subtractor int // tracks how much to subtract from expected indentation multiplier
var lastPrefixIndex int // tracks the index (in both skippedDirList and dirList) of the last displayed parent directory
var trimmedDirectory = dirList[currentDirIndex]
trimmed := dirList[currentDirIndex]
indent := strings.Count(trimmed, "/") - 1 // avoid indenting root-level dirs
// determine initial indentation multiplier based on "/" occurrences
indent := strings.Count(trimmedDirectory, "/") - 1 // subtract 1 to avoid indenting root-level directories
for i, skipped := range skippedDirList[:currentDirIndex] { // checks each skipped directory to determine if it is a parent to the current directory
if strings.HasPrefix(trimmedDirectory, dirList[i]+"/") { // if the current directory is the child of this iteration's directory...
if skipped { // ...and this iteration's directory was skipped...
subtractor++ // increment the subtractor to indicate that the visual indentation should be reduced
subtractor, lastPrefixIndex := 0, 0
for i, skipped := range skippedDirList[:currentDirIndex] {
if strings.HasPrefix(trimmed, dirList[i]+"/") {
if skipped {
subtractor++
} else {
lastPrefixIndex = i
}
}
}
indent = indent - subtractor // calculates final visual indentation multiplier
if indent < 0 { // disallow negative indentation multipliers
indent = 0
} else if indent > 0 { // trim the most recently displayed parent directory from the directory header to avoid displaying redundant information
trimmedDirectory = strings.Replace(trimmedDirectory, dirList[lastPrefixIndex], "", 1)
if indent -= subtractor; indent < 0 {
return 0, trimmed
}
return indent, trimmedDirectory
if indent > 0 {
trimmed = strings.TrimPrefix(trimmed, dirList[lastPrefixIndex])
}
return indent, trimmed
}
// printFileEntry handles processing for printing file entries (determines color, wraps lines, and prints).
func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlternator int8) (int, int8) {
// determine color to print fileEntryName (alternate each time function is run)
var colorCode string
if colorAlternator > 0 {
colorCode = ""
} else {
// printFileEntry handles processing for printing file entries (determines color, adds aging indicator, wraps lines, and prints).
func printFileEntry(sb *strings.Builder, entry string, lastSlash int, charCounter *int, indent int, colorAlternator int8, agingTimestamp *int64) int8 {
colorCode := ""
if colorAlternator <= 0 {
colorCode = ansiAlternateEntryColor
}
colorAlternator = -colorAlternator
// trim the containing directory from the entry to determine fileEntryName
fileEntryName := entry[lastSlash:]
if charCounter == 0 { // indent first line of entries for each directory header
fmt.Print(strings.Repeat(" ", indent*2))
agingDot := ""
switch age.TranslateAgeTimestamp(agingTimestamp) {
case 1:
agingDot = back.AnsiGreen + "⁍" + back.AnsiReset
case 2:
agingDot = back.AnsiWarning + "⁍" + back.AnsiReset
case 3:
agingDot = back.AnsiError + "⁍" + back.AnsiReset
}
// determine whether to wrap to a new line (+1 is to account for trailing spaces)
charCounter += len(fileEntryName) + 1
if indentation := indent * 2; charCounter+(indentation) >= width {
charCounter = len(fileEntryName) + 1
fmt.Print("\n" + strings.Repeat(" ", indentation)) // indent each line
name := entry[lastSlash:]
if *charCounter == 0 {
writeIndent(sb, indent)
}
// print fileEntryName to screen
fmt.Printf("%s%s%s ", colorCode, fileEntryName, back.AnsiReset)
// wrap if needed; +1 accounts for trailing space, +1 more if aging dot is present
inc := len(name) + 1
if agingDot != "" {
inc++
}
if *charCounter += inc; *charCounter+indent*2 >= width {
*charCounter = len(name) + 1
if agingDot != "" {
*charCounter++
}
sb.WriteString("\n")
writeIndent(sb, indent)
}
return charCounter, colorAlternator
sb.WriteString(agingDot)
sb.WriteString(colorCode)
sb.WriteString(name)
sb.WriteString(back.AnsiReset)
sb.WriteString(" ")
return colorAlternator
}
// EntryListGen generates and displays the full libmutton entry list.
func EntryListGen() {
fileList, dirList, err := synccommon.WalkEntryDir()
_, dirList, err := synccommon.WalkEntryDir()
if err != nil {
other.PrintError("Failed to generate entry list: "+err.Error(), back.ErrorRead)
}
entryMap, err := synccommon.GetAllEntryData()
if err != nil {
other.PrintError("Failed to retrieve entry aging data: "+err.Error(), back.ErrorRead)
}
var sb strings.Builder
// print header bar w/total entry count
fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset)
sb.WriteString("\n")
sb.WriteString(ansiBlackOnWhite)
_, _ = fmt.Fprint(&sb, len(entryMap))
sb.WriteString(" libmutton entries:")
sb.WriteString(back.AnsiReset)
// dirList iteration
dirListLength := len(dirList) // save length for multiple references below
var skippedDirList = make([]bool, dirListLength) // stores whether each directory was skipped during printout (later used to determine appropriate visual indentation)
charCounter := 0 // track whether to line-wrap based on character count in line
var colorAlternator int8 = 1 // track alternating colors for each printed entry name
var containsSubdirectory bool // indicates whether the current directory contains a subdirectory
var indent int // visual indentation multiplier
var vanityDirectory string // directory header printed to end-user - visual only, not used in any processing
for i, directory := range dirList {
// reset formatting variables for new directory
charCounter = 0
colorAlternator = 1
// default to assuming this directory will be skipped (unless it is the root)
if i == 0 {
skippedDirList[i] = false
} else {
skippedDirList[i] = true
entriesByDir := make(map[string][]string, len(dirList))
for vanityPath := range entryMap {
if lastSlash := strings.LastIndex(vanityPath, "/"); lastSlash >= 0 {
entriesByDir[vanityPath[:lastSlash]] = append(entriesByDir[vanityPath[:lastSlash]], vanityPath)
}
}
for _, entries := range entriesByDir {
sort.Strings(entries)
}
// check if next directory is within the current one
if dirListLength > i+1 {
if nextDir := dirList[i+1]; directory == nextDir[:strings.LastIndex(nextDir, "/")] {
containsSubdirectory = true
} else {
containsSubdirectory = false
}
} else {
containsSubdirectory = false
}
// fileList iteration
containsFiles := false // indicates whether the current directory contains files (entries)
for _, file := range fileList {
// print the current file if it belongs in the current directory - otherwise, break the loop and move on to the next directory
if lastSlash := strings.LastIndex(file, "/") + 1; file[:lastSlash-1] == directory {
// print directory header if this is the first run of the loop
if !containsFiles {
containsFiles = true
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
}
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator)
}
}
if !containsFiles { // if the current directory contains no files...
if !containsSubdirectory { // nor does it contain any subdirectories...
if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + back.AnsiReset)
} else { // warn if the only thing that exists is the root-level directory
fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
}
}
// precompute whether each directory contains subdirectories;
// handle root-level child dirs by treating "" as their parent.
hasChildDir := make(map[string]bool, len(dirList))
for _, d := range dirList {
switch parentEnd := strings.LastIndex(d, "/"); {
case parentEnd > 0:
hasChildDir[d[:parentEnd]] = true
case parentEnd == -1:
hasChildDir[""] = true
}
}
// print trailing new lines for proper spacing after entry list is complete
fmt.Print("\n\n")
dirListLength := len(dirList)
skippedDirList := make([]bool, dirListLength)
charCounter := 0
var colorAlternator int8 = 1
printDirHeader := func(i int) int {
skippedDirList[i] = false
indent, vanityDirectory := determineIndentation(skippedDirList, dirList, i)
sb.WriteString("\n\n")
writeIndent(&sb, indent)
sb.WriteString(ansiDirectoryHeader)
sb.WriteString(vanityDirectory)
sb.WriteString("/" + back.AnsiReset + "\n")
return indent
}
for i, directory := range dirList {
charCounter, colorAlternator = 0, 1
skippedDirList[i] = i != 0
if entries := entriesByDir[directory]; len(entries) > 0 {
indent := printDirHeader(i)
for _, vanityPath := range entries {
lastSlash := strings.LastIndex(vanityPath, "/") + 1
colorAlternator = printFileEntry(&sb, vanityPath, lastSlash, &charCounter, indent, colorAlternator, entryMap[vanityPath].AgeTimestamp)
}
continue
}
if hasChildDir[directory] {
continue
}
if dirListLength > 1 {
indent := printDirHeader(i)
writeIndent(&sb, indent)
sb.WriteString(back.AnsiWarning + "-empty directory-" + back.AnsiReset)
continue
}
sb.WriteString("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
}
sb.WriteString("\n\n")
fmt.Print(sb.String())
os.Exit(0)
}
+20 -37
View File
@@ -2,7 +2,6 @@ package cli
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/glamour"
@@ -10,59 +9,53 @@ import (
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/crypt"
"github.com/rwinkhart/libmutton/global"
"github.com/rwinkhart/libmutton/syncclient"
)
const ansiShownPassword = "\033[38;5;10m"
// EntryReader prints the decrypted contents of a libmutton entry in a human-readable format.
func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
fmt.Println()
func EntryReader(vanityPath string, decSlice []string, hideSecrets bool) {
fmt.Print("\n" + back.AnsiBold + vanityPath + back.AnsiReset + "\n\n")
fieldLoop:
for field := range decryptedEntry {
for field := range decSlice {
switch field {
case 0:
// if the first field (password) is not empty, print it
if decryptedEntry[0] != "" {
if decSlice[0] != "" {
if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + back.AnsiReset + "\n\n")
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[0] + back.AnsiReset + "\n\n")
} else {
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
}
}
case 1:
// if the second field (username) is not empty, print it
if decryptedEntry[1] != "" {
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decryptedEntry[1] + "\n\n")
if decSlice[1] != "" {
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decSlice[1] + "\n\n")
}
case 2:
// if the third field (TOTP secret) is not empty, print it
if decryptedEntry[2] != "" {
if decSlice[2] != "" {
if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + back.AnsiReset + "\n\n")
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[2] + back.AnsiReset + "\n\n")
} else {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
}
}
case 3:
// if the fourth field (url) is not empty, print it
if decryptedEntry[3] != "" {
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decryptedEntry[3] + "\n\n")
if decSlice[3] != "" {
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decSlice[3] + "\n\n")
}
case 4:
// print the notes header
fmt.Println(ansiDirectoryHeader + "Notes:" + back.AnsiReset)
// combine remaining fields into a single string (to support Markdown rendering)
var noteLines []string
for ; field < len(decryptedEntry); field++ {
noteLines = append(noteLines, decryptedEntry[field])
}
// render notes as Markdown
r, _ := glamour.NewTermRenderer(glamour.WithStylesFromJSONBytes(glamourStyle()), glamour.WithPreservedNewLines(), glamour.WithWordWrap(width))
markdownNotesString, _ := r.Render(strings.Join(noteLines, "\n"))
r, _ := glamour.NewTermRenderer(glamour.WithStylesFromJSONBytes(glamourStyle()), glamour.WithWordWrap(width))
markdownNotesString, _ := r.Render(strings.Join(decSlice[4:], "\n"))
_ = r.Close()
// print the rendered Markdown notes
fmt.Print(markdownNotesString)
@@ -71,27 +64,17 @@ fieldLoop:
break fieldLoop
}
}
if syncEnabled {
_, err := syncclient.RunJob(false)
if err != nil {
other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess)
}
}
os.Exit(0)
}
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
_, err := back.TargetIsFile(targetLocation, true)
func EntryReaderDecrypt(realPath string, hideSecrets bool) {
_, err := back.TargetIsFile(realPath, true)
if err != nil { // if the location does not exist or is a directory...
other.PrintError("Failed to verify target location: "+err.Error(), back.ErrorTargetNotFound)
}
decBytes, err := crypt.DecryptFileToSlice(targetLocation)
decSlice, err := crypt.DecryptFileToSlice(realPath, nil)
if err != nil {
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
}
EntryReader(decBytes, hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
// do not exit, as this is the job of EntryReader
EntryReader(global.GetVanityPath(realPath), decSlice, hideSecrets)
}
+9 -7
View File
@@ -15,7 +15,7 @@ const (
)
func HelpMain() {
fmt.Print(back.AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + back.AnsiReset + `
fmt.Print(back.AnsiBold + "\nMUTN | Copyright (c) 2024-2026 Randall Winkhart\n" + back.AnsiReset + `
This software exists under the MIT license; you may redistribute it under certain conditions.
This program comes with absolutely no warranty; type "mutn version" for details.
@@ -40,6 +40,7 @@ This program comes with absolutely no warranty; type "mutn version" for details.
totp|-t Generate and copy the TOTP token for an entry to your clipboard
url|-l Copy the URL in an entry to your clipboard
note|-n Copy the first note line in an entry to your clipboard
menu|-m Open interactive menu to copy any combination of fields
edit:
password|-pw|<blank> Change the password in an entry
username|-u Change the username in an entry
@@ -55,9 +56,9 @@ This program comes with absolutely no warranty; type "mutn version" for details.
folder|-f Add a new folder for entries
` + back.AnsiBold + "Tip 1:" + back.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>"
` + back.AnsiBold + "Tip 2:" + back.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries
` + back.AnsiBold + "Tip 3:" + back.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
` + back.AnsiBold + "Tip 4:" + back.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n")
` + back.AnsiBold + "Tip 2:" + back.AnsiReset + ` Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
` + back.AnsiBold + "Tip 3:" + back.AnsiReset + ` Various changes can be made to the configuration and stored entries from the "mutn tweak" menu
` + back.AnsiBold + "Tip 4:" + back.AnsiReset + " Type \"mutn\" (no arguments) to view a list of saved entries w/colored markers indicating the ages of stored passwords\n\n")
os.Exit(0)
}
@@ -95,7 +96,8 @@ func HelpCopy() {
username|-u Copy the username in an entry to your clipboard
totp|-t Generate and copy the TOTP token for an entry to your clipboard
url|-l Copy the URL in an entry to your clipboard
note|-n Copy the first note line in an entry to your clipboard` + "\n\n")
note|-n Copy the first note line in an entry to your clipboard
menu|-m Open interactive menu to copy any combination of fields` + "\n\n")
os.Exit(0)
}
@@ -150,11 +152,11 @@ func Version() {
ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" +
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " MUTN v" + MUTNVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " The Cryptic Cuts Update " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " The \"Hardy Herd\" Update " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Built with libmutton v" + global.LibmuttonVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Copyright (c) 2024-2025: Randall Winkhart " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Copyright (c) 2024-2026: Randall Winkhart " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + back.AnsiReset +
"\n For more information, see:\n\n" +
+8 -12
View File
@@ -2,17 +2,16 @@ package cli
import (
"fmt"
"strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/go-boilerplate/security"
"github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/synccycles"
)
// inputPasswordGen prompts the user for password generation parameters and returns a generated password as a string.
func inputPasswordGen() string {
func inputPasswordGen() []byte {
passLength := front.InputInt("Password length:", 1, -1)
fmt.Println()
passCharset := uint8(front.InputMenuGen("Password complexity:", []string{"Simple", "Complex", "Ultra Complex (not compatible with many services)"}))
@@ -25,20 +24,17 @@ func inputPasswordGen() string {
complexity = 0.2 // (ultra) complex
// 2 and 3 indicate complex and ultra complex charsets, respectively
}
return synccycles.StringGen(passLength, complexity, passCharset)
return security.BytesGen(passLength, complexity, passCharset)
}
// writeEntryCLI writes an entry to targetLocation and previews it (errors if no data is supplied).
func writeEntryCLI(targetLocation string, decryptedEntry []string, hideSecrets bool) {
if core.EntryIsNotEmpty(decryptedEntry) {
// write the entry to the target location
err := core.WriteEntry(targetLocation, []byte(strings.Join(decryptedEntry, "\n")))
if err != nil {
// writeEntryCLI writes decSlice to realPath and previews it (errors if no data is supplied).
func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword []byte) {
if core.EntryIsNotEmpty(decSlice) {
if err := core.WriteEntry(realPath, decSlice, passwordIsNew, nil); err != nil {
other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite)
}
// preview the entry
fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset)
EntryReader(decryptedEntry, hideSecrets, true)
CopyMenu("", decSlice, oldPassword)
} else {
other.PrintError("No data supplied for entry", back.ErrorTargetNotFound)
}
-1
View File
@@ -6,4 +6,3 @@ CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
Additionally, some custom build tags can be used to create different binaries. The following tags are not used in official builds:
- `wsl`: Allows creating a Linux binary that can interact with the Windows clipboard (for WSL)
- `termux`: Allows creating a Linux binary that can interact with the Termux clipboard (for Android)
+1 -1
View File
@@ -14,4 +14,4 @@ Please see the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/us
### Dependencies (required for all installations)
- A text editor (preferably CLI-based) for writing entry notes
- A private key for SSH key-based authentication is required to use MUTN/libmutton in online/synced mode
- Linux: xclip (X11) or wl-clipboard (Wayland) for clipboard support
- Linux/BSD: xclip (X11) or wl-clipboard (Wayland) for clipboard support
+1
View File
@@ -15,6 +15,7 @@ Once this has been done, you may proceed with the client setup:
2. Run `mutn init` and follow the prompts
3. If you already have entries on the server, sync them using `mutn sync`
4. Optionally, shell completions (Bash, ZSH, and PowerShell 7+) can be [enabled with your shell's respective method](https://github.com/rwinkhart/MUTN/blob/main/wiki/completions.md)
5. Optionally, [import passwords](https://github.com/rwinkhart/libmutton/blob/main/wiki/migration.md) from another password manager. After doing this, you may desire to use the "Age all entries" option from the `mutn tweak` menu to add initial age tracking data to the imported passwords
### Tips
#### Operating on Passwords