Sync with latest libmutton development version; draft documentation for release v0.4.0

This commit is contained in:
2026-01-08 21:23:36 -05:00
parent 9af456590c
commit 32364717dc
8 changed files with 39 additions and 40 deletions
-9
View File
@@ -23,16 +23,7 @@ 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). After installing, please review the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/usage.md).
# Roadmap # Roadmap
### Release v0.4.0
- Add interactive copy menu
- Send user to this menu after add/gen
- libmutton v0.5.0
- Password aging support
- Add green/yellow/red dot indicators to entry list readout for when passwords should be changed
### Release v0.5.0 ### Release v0.5.0
- libmutton v0.6.0
- Implement "netpin" (quick-unlock)
### Release v0.6.0
- Switch to fully compliant Markdown (do not preserve new lines) - Switch to fully compliant Markdown (do not preserve new lines)
- Switch to alternative Markdown renderer (minimark or BEAN) - Switch to alternative Markdown renderer (minimark or BEAN)
### Release v1.0.0 - Distribution packages ### Release v1.0.0 - Distribution packages
+22 -1
View File
@@ -4,4 +4,25 @@
Built with libmutton v0.5.0 Built with libmutton v0.5.0
Release date TBD Release date TBD
This release syncs with libmutton v0.5.0 and adds a new multi-field copy menu. # The ??? 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
## Dependencies (direct/replaced)
- Dropped (libmutton)
- golang.design/x/clipboard
- gopkg.in/ini.v1
- Bumped
- Go: v1.25.4 => 1.25.5
- github.com/rwinkhart/go-boilerplate: v0.1.0 => v0.1.1
- github.com/rwinkhart/libmutton: v0.4.4 => v0.5.0
- golang.org/x/term: v0.37.0 => v0.38.0
- golang.org/x/sys: v0.38.0 => v0.40.0
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.25.5
require ( require (
github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/glamour v0.7.0
github.com/rwinkhart/go-boilerplate v0.1.1 github.com/rwinkhart/go-boilerplate v0.1.1
github.com/rwinkhart/libmutton v0.4.3-0.20260108232004-d54b59030e72 github.com/rwinkhart/libmutton v0.4.3-0.20260109022300-a54d15e33111
golang.org/x/term v0.38.0 golang.org/x/term v0.38.0
) )
+2 -2
View File
@@ -48,8 +48,8 @@ github.com/rwinkhart/go-highlite v0.1.1 h1:9TxbRhYVfD/3YaEgNk1BtEiZ0t8/5mxDUZqNM
github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA= github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA=
github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg= github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg=
github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/rwinkhart/libmutton v0.4.3-0.20260108232004-d54b59030e72 h1:sFtwqO//25lLwsD77wtxyojRKSrK+ABVZSWIZg5mf1w= github.com/rwinkhart/libmutton v0.4.3-0.20260109022300-a54d15e33111 h1:nxQHhtdRVIUhRAc/5oYTli30tive65c8N7u75dENVWE=
github.com/rwinkhart/libmutton v0.4.3-0.20260108232004-d54b59030e72/go.mod h1:qInUv19lzX1adFNI6Ih/IEVy+pktSTh/yA7cRTdVPH8= github.com/rwinkhart/libmutton v0.4.3-0.20260109022300-a54d15e33111/go.mod h1:qInUv19lzX1adFNI6Ih/IEVy+pktSTh/yA7cRTdVPH8=
github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo= github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo=
github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c= github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c=
github.com/rwinkhart/rcw v0.2.4 h1:FrRSsl+ui1WLmDwqjLSROtFPKqSeGLEfF2SPENt4qP0= github.com/rwinkhart/rcw v0.2.4 h1:FrRSsl+ui1WLmDwqjLSROtFPKqSeGLEfF2SPENt4qP0=
+9 -17
View File
@@ -48,8 +48,7 @@ func main() {
case "show", "-s": case "show", "-s":
cli.EntryReaderDecrypt(realPath, false) cli.EntryReaderDecrypt(realPath, false)
case "copy": case "copy":
err := clip.CopyShortcut(realPath, 0) if err := clip.CopyShortcut(realPath, 0); err != nil {
if err != nil {
other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard)
} }
case "edit": case "edit":
@@ -59,8 +58,7 @@ func main() {
case "add": case "add":
cli.AddEntry(realPath, 0) cli.AddEntry(realPath, 0)
case "shear": case "shear":
err := syncclient.ShearRemote(args[1], false) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory if err := syncclient.ShearRemote(args[1], false); err != nil {
if err != nil {
other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite)
} }
default: default:
@@ -100,8 +98,7 @@ func main() {
default: default:
cli.HelpCopy() cli.HelpCopy()
} }
err := clip.CopyShortcut(realPath, field) if err := clip.CopyShortcut(realPath, field); err != nil {
if err != nil {
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
} }
case "edit": case "edit":
@@ -169,8 +166,7 @@ func main() {
case "note", "-n": case "note", "-n":
cli.AddEntry(realPath, 2) cli.AddEntry(realPath, 2)
case "folder", "-f": case "folder", "-f":
err := syncclient.AddFolderRemote(args[1]) // pass the incomplete path as the server will have a different home directory if err := syncclient.AddFolderRemote(args[1]); err != nil {
if err != nil {
other.PrintError("Failed to add folder: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to add folder: "+err.Error(), back.ErrorWrite)
} }
default: default:
@@ -185,8 +181,7 @@ func main() {
} else { } else {
switch args[1] { switch args[1] {
case "clipclear": case "clipclear":
err := clip.ClearArgument() if err := clip.ClearArgument(); err != nil {
if err != nil {
other.PrintError("Failure occurred in clipboard clearing process: "+err.Error(), global.ErrorClipboard) other.PrintError("Failure occurred in clipboard clearing process: "+err.Error(), global.ErrorClipboard)
} }
case "startrcwd": case "startrcwd":
@@ -215,8 +210,7 @@ func main() {
newCfg := &config.CfgT{} newCfg := &config.CfgT{}
newClientSpecificCfg := map[string]any{"mutnTextEditor": getTextEditorInput()} newClientSpecificCfg := map[string]any{"mutnTextEditor": getTextEditorInput()}
newCfg.ClientSpecific = &newClientSpecificCfg newCfg.ClientSpecific = &newClientSpecificCfg
err := config.Write(newCfg, true) if err := config.Write(newCfg, true); err != nil {
if err != nil {
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
} }
case 2: case 2:
@@ -224,7 +218,7 @@ func main() {
if err != nil { if err != nil {
other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead) other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead)
} }
_, _, err = syncclient.GenDeviceID(oldDeviceID, "") _, _, _, err = syncclient.GenDeviceID(oldDeviceID, "")
if err != nil { if err != nil {
other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess) other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess)
} }
@@ -232,16 +226,14 @@ func main() {
case 3: case 3:
forceReage := front.InputBinary("Re-age aged entries?") forceReage := front.InputBinary("Re-age aged entries?")
fmt.Println("Aging entries; this may take awhile - do not terminate this process") fmt.Println("Aging entries; this may take awhile - do not terminate this process")
err := age.AllPasswordEntries(forceReage) if err := age.AllPasswordEntries(forceReage); err != nil {
if err != nil {
other.PrintError("Failed to age entries: "+err.Error(), 1) other.PrintError("Failed to age entries: "+err.Error(), 1)
} }
case 4: case 4:
oldPassword := confirmRCWPassword("old") oldPassword := confirmRCWPassword("old")
newPassword := confirmRCWPassword("new") newPassword := confirmRCWPassword("new")
fmt.Print("\nRe-encrypting entries. Please wait; do not force close this process.\n") fmt.Print("\nRe-encrypting entries. Please wait; do not force close this process.\n")
err := core.EntryRefresh(oldPassword, newPassword, false) if err := core.EntryRefresh(oldPassword, newPassword, false); err != nil {
if err != nil {
other.PrintError("Re-encryption failed: "+err.Error(), global.ErrorEncryption) other.PrintError("Re-encryption failed: "+err.Error(), global.ErrorEncryption)
} }
fmt.Println("\nRe-encryption complete.") fmt.Println("\nRe-encryption complete.")
+2 -4
View File
@@ -102,8 +102,7 @@ func CopyMenu(vanityPath string, decSlice []string, oldPassword string) {
} }
selectedField = fieldOptions[choice-1] selectedField = fieldOptions[choice-1]
if selectedField == "Old Password" { if selectedField == "Old Password" {
err = clip.CopyString(false, oldPassword) if err = clip.CopyString(false, oldPassword); err != nil {
if err != nil {
other.PrintError("Failed to copy old password to clipboard: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy old password to clipboard: "+err.Error(), global.ErrorClipboard)
} }
continue continue
@@ -115,8 +114,7 @@ func CopyMenu(vanityPath string, decSlice []string, oldPassword string) {
done := make(chan bool) done := make(chan bool)
go clip.TOTPCopier(decSlice[2], errorChan, done) go clip.TOTPCopier(decSlice[2], errorChan, done)
// block until first successful copy // block until first successful copy
err = <-errorChan if err = <-errorChan; err != nil {
if err != nil {
other.PrintError("Failed to copy TOTP token: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy TOTP token: "+err.Error(), global.ErrorClipboard)
} }
fmt.Println(back.AnsiGreen + "[Started]" + back.AnsiReset + " TOTP clipboard refresher") fmt.Println(back.AnsiGreen + "[Started]" + back.AnsiReset + " TOTP clipboard refresher")
+2 -4
View File
@@ -18,8 +18,7 @@ import (
func RenameCli(oldVanityPath string) { func RenameCli(oldVanityPath string) {
// prompt user for new location and rename // prompt user for new location and rename
newVanityPath := front.Input("New location:") newVanityPath := front.Input("New location:")
err := syncclient.RenameRemote(oldVanityPath, newVanityPath) if err := syncclient.RenameRemote(oldVanityPath, newVanityPath); err != nil {
if err != nil {
other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite)
} }
@@ -131,8 +130,7 @@ func editNote(baseNote []string) ([]string, bool) {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err = cmd.Run() if err = cmd.Run(); err != nil {
if 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 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
} }
+1 -2
View File
@@ -30,8 +30,7 @@ func inputPasswordGen() string {
// writeEntryCLI writes an entry to realPath and previews it (errors if no data is supplied). // writeEntryCLI writes an entry to realPath and previews it (errors if no data is supplied).
func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword string) { func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword string) {
if core.EntryIsNotEmpty(decSlice) { if core.EntryIsNotEmpty(decSlice) {
err := core.WriteEntry(realPath, decSlice, passwordIsNew) if err := core.WriteEntry(realPath, decSlice, passwordIsNew); err != nil {
if err != nil {
other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite)
} }
// preview the entry // preview the entry