Switch to dependence on github.com/rwinkhart/libmutton

This commit is contained in:
2024-08-08 17:24:48 -04:00
parent 57378c521a
commit a23ae4a4b9
11 changed files with 114 additions and 110 deletions
+6 -5
View File
@@ -4,12 +4,8 @@ go 1.22.6
require ( require (
github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/glamour v0.7.0
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727 github.com/rwinkhart/libmutton v0.0.0-20240808205713-d82188f56655
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481
golang.org/x/crypto v0.26.0
golang.org/x/term v0.23.0 golang.org/x/term v0.23.0
gopkg.in/ini.v1 v1.67.0
) )
require ( require (
@@ -18,6 +14,7 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.2 // indirect github.com/boombuler/barcode v1.0.2 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/kr/fs v0.1.0 // 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.2.0 // indirect
@@ -27,10 +24,14 @@ require (
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/sftp v1.13.6 // indirect
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect
github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.3 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.23.0 // indirect golang.org/x/sys v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
) )
+2
View File
@@ -52,6 +52,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rwinkhart/libmutton v0.0.0-20240808205713-d82188f56655 h1:nlvJc8lkwZcuIWsiIl1qh6g7A/oCLnRz7mnR7oEdlyA=
github.com/rwinkhart/libmutton v0.0.0-20240808205713-d82188f56655/go.mod h1:treJ/J2D6+LuDQQ435Jg3fsXRHlBnoKlMYExo3VzUKc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+10 -10
View File
@@ -7,9 +7,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend"
"github.com/rwinkhart/MUTN/src/cli" "github.com/rwinkhart/MUTN/src/cli"
"github.com/rwinkhart/MUTN/src/sync" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync"
) )
func main() { func main() {
@@ -55,13 +55,13 @@ func main() {
case "register": case "register":
// register a new device ID // register a new device ID
// stdin[0] is expected to be the device ID // stdin[0] is expected to be the device ID
_, _ = os.Create(backend.ConfigDir + backend.PathSeparator + "devices" + backend.PathSeparator + stdin[0]) // error ignored; failure unlikely to occur if init was successful; "register" is not a user-facing argument and thus the error would not be visible _, _ = os.Create(core.ConfigDir + core.PathSeparator + "devices" + core.PathSeparator + stdin[0]) // error ignored; failure unlikely to occur if init was successful; "register" is not a user-facing argument and thus the error would not be visible
// print EntryRoot and bool indicating OS type to stdout for client to store in config // print EntryRoot and bool indicating OS type to stdout for client to store in config
fmt.Print(backend.EntryRoot + sync.FSSpace + strconv.FormatBool(backend.IsWindows)) fmt.Print(core.EntryRoot + sync.FSSpace + strconv.FormatBool(core.IsWindows))
case "init": case "init":
// create the necessary directories for libmuttonserver to function // create the necessary directories for libmuttonserver to function
backend.DirInit(false) core.DirInit(false)
_ = os.MkdirAll(backend.ConfigDir+backend.PathSeparator+"deletions", 0700) // error ignored; failure would have occurred by this point in backend.DirInit _ = os.MkdirAll(core.ConfigDir+core.PathSeparator+"deletions", 0700) // error ignored; failure would have occurred by this point in core.DirInit
fmt.Println("libmuttonserver directories initialized") fmt.Println("libmuttonserver directories initialized")
case "version": case "version":
versionServer() versionServer()
@@ -71,13 +71,13 @@ func main() {
} }
func helpServer() { func helpServer() {
fmt.Print(cli.AnsiBold + "\nlibmuttonserver | Copyright (c) 2024 Randall Winkhart\n" + backend.AnsiReset + ` fmt.Print(cli.AnsiBold + "\nlibmuttonserver | Copyright (c) 2024 Randall Winkhart\n" + core.AnsiReset + `
This software exists under the MIT license; you may redistribute it under certain conditions. This software exists under the MIT license; you may redistribute it under certain conditions.
This program comes with absolutely no warranty; type "libmuttonserver version" for details. This program comes with absolutely no warranty; type "libmuttonserver version" for details.
` + cli.AnsiBold + "Usage:" + backend.AnsiReset + ` libmuttonserver <argument> ` + cli.AnsiBold + "Usage:" + core.AnsiReset + ` libmuttonserver <argument>
` + cli.AnsiBold + "Arguments (user):" + backend.AnsiReset + ` ` + cli.AnsiBold + "Arguments (user):" + core.AnsiReset + `
help Bring up this menu help Bring up this menu
version Display version and license information version Display version and license information
init Create the necessary directories for libmuttonserver to function` + "\n\n") init Create the necessary directories for libmuttonserver to function` + "\n\n")
@@ -86,7 +86,7 @@ This program comes with absolutely no warranty; type "libmuttonserver version" f
func versionServer() { func versionServer() {
cli.MITLicense() cli.MITLicense()
fmt.Print(cli.AnsiBold + "\n\n libmuttonserver" + backend.AnsiReset + " Version " + backend.LibmuttonVersion + ` fmt.Print(cli.AnsiBold + "\n\n libmuttonserver" + core.AnsiReset + " Version " + core.LibmuttonVersion + `
Copyright (c) 2024 Randall Winkhart` + "\n\n") Copyright (c) 2024 Randall Winkhart` + "\n\n")
} }
+8 -8
View File
@@ -5,9 +5,9 @@ import (
"os" "os"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend"
"github.com/rwinkhart/MUTN/src/cli" "github.com/rwinkhart/MUTN/src/cli"
"github.com/rwinkhart/MUTN/src/sync" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync"
) )
func main() { func main() {
@@ -25,7 +25,7 @@ func main() {
if strings.HasPrefix(args[1], "/") { if strings.HasPrefix(args[1], "/") {
// store location of target entry // store location of target entry
targetLocation := backend.TargetLocationFormat(args[1]) targetLocation := core.TargetLocationFormat(args[1])
// entry reader shortcut (if no other arguments are supplied) // entry reader shortcut (if no other arguments are supplied)
if argsCount == 2 { if argsCount == 2 {
@@ -38,7 +38,7 @@ func main() {
case "show", "-s": case "show", "-s":
cli.EntryReaderDecrypt(targetLocation, false) cli.EntryReaderDecrypt(targetLocation, false)
case "copy": case "copy":
backend.CopyArgument(args[0], targetLocation, 0) core.CopyArgument(args[0], targetLocation, 0)
case "edit": case "edit":
cli.EditEntryField(targetLocation, true, 0) cli.EditEntryField(targetLocation, true, 0)
case "gen": case "gen":
@@ -82,7 +82,7 @@ func main() {
default: default:
cli.HelpCopy() cli.HelpCopy()
} }
backend.CopyArgument(args[0], targetLocation, field) core.CopyArgument(args[0], targetLocation, field)
case "edit": case "edit":
var field int // indicates which (numbered) field to edit var field int // indicates which (numbered) field to edit
switch args[3] { switch args[3] {
@@ -97,7 +97,7 @@ func main() {
case "note", "-n": case "note", "-n":
field = 4 field = 4
case "rename", "-r": case "rename", "-r":
backend.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location core.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location
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 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: default:
cli.HelpEdit() cli.HelpEdit()
@@ -158,13 +158,13 @@ func main() {
} else { } else {
switch args[1] { switch args[1] {
case "clipclear": case "clipclear":
backend.ClipClearArgument() core.ClipClearArgument()
case "sync": case "sync":
sync.RunJob(true) sync.RunJob(true)
case "init": case "init":
cli.TempInitCli() cli.TempInitCli()
case "tweak": case "tweak":
fmt.Println(backend.AnsiError + "\"tweak\" is not yet implemented" + backend.AnsiReset) fmt.Println(core.AnsiError + "\"tweak\" is not yet implemented" + core.AnsiReset)
os.Exit(0) os.Exit(0)
case "copy": case "copy":
cli.HelpCopy() cli.HelpCopy()
+5 -5
View File
@@ -5,21 +5,21 @@ import (
"os" "os"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend" "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 targetLocation by taking user input via CLI prompts
// entryType: 0 = standard (password), 1 = auto-generated password, 2 = note // entryType: 0 = standard (password), 1 = auto-generated password, 2 = note
func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) { func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
// ensure target location does not already exist // ensure target location does not already exist
_, isAccessible := backend.TargetIsFile(targetLocation, false, 0) _, isAccessible := core.TargetIsFile(targetLocation, false, 0)
if isAccessible { if isAccessible {
fmt.Println(backend.AnsiError + "Target location already exists" + backend.AnsiReset) fmt.Println(core.AnsiError + "Target location already exists" + core.AnsiReset)
os.Exit(1) os.Exit(1)
} }
// ensure target containing directory exists and is a directory (not a file) // ensure target containing directory exists and is a directory (not a file)
backend.TargetIsFile(targetLocation[:strings.LastIndex(targetLocation, "/")], true, 1) core.TargetIsFile(targetLocation[:strings.LastIndex(targetLocation, "/")], true, 1)
var unencryptedEntry []string var unencryptedEntry []string
@@ -31,7 +31,7 @@ func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
if entryType == 0 { if entryType == 0 {
password = inputHidden("Password:") password = inputHidden("Password:")
} else { } else {
password = backend.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2, false) password = core.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2, false)
} }
totp := inputHidden("TOTP secret:") totp := inputHidden("TOTP secret:")
+11 -11
View File
@@ -8,8 +8,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/MUTN/src/sync" "github.com/rwinkhart/libmutton/sync"
) )
// RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server // RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server
@@ -24,7 +24,7 @@ func RenameCli(oldLocationIncomplete string) {
// EditEntryField edits a field of an entry at targetLocation (user input) // EditEntryField edits a field of an entry at targetLocation (user input)
func EditEntryField(targetLocation string, hideSecrets bool, field int) { func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// fetch old entry data (with all required lines present) // fetch old entry data (with all required lines present)
unencryptedEntry := backend.GetOldEntryData(targetLocation, field) unencryptedEntry := core.GetOldEntryData(targetLocation, field)
// edit the field // edit the field
switch field { switch field {
@@ -44,7 +44,7 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// edit the note // edit the note
editedNote, noteEdited := editNote(noteData) editedNote, noteEdited := editNote(noteData)
if !noteEdited { // exit early if the note was not edited if !noteEdited { // exit early if the note was not edited
fmt.Println(backend.AnsiError + "Entry is unchanged" + backend.AnsiReset) fmt.Println(core.AnsiError + "Entry is unchanged" + core.AnsiReset)
os.Exit(1) os.Exit(1)
} }
unencryptedEntry = append(nonNoteData, editedNote...) unencryptedEntry = append(nonNoteData, editedNote...)
@@ -57,10 +57,10 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// GenUpdate generates a new password for an entry at targetLocation (user input) // GenUpdate generates a new password for an entry at targetLocation (user input)
func GenUpdate(targetLocation string, hideSecrets bool) { func GenUpdate(targetLocation string, hideSecrets bool) {
// fetch old entry data // fetch old entry data
unencryptedEntry := backend.GetOldEntryData(targetLocation, 0) unencryptedEntry := core.GetOldEntryData(targetLocation, 0)
// generate a new password // generate a new password
unencryptedEntry[0] = backend.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2, false) unencryptedEntry[0] = core.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2, false)
// write and preview the modified entry // write and preview the modified entry
writeEntryCLI(targetLocation, unencryptedEntry, hideSecrets, false) writeEntryCLI(targetLocation, unencryptedEntry, hideSecrets, false)
@@ -69,13 +69,13 @@ func GenUpdate(targetLocation string, hideSecrets bool) {
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty) // editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty)
// returns the edited note and a boolean indicating whether the note was edited // returns the edited note and a boolean indicating whether the note was edited
func editNote(baseNote []string) ([]string, bool) { func editNote(baseNote []string) ([]string, bool) {
tempFile := backend.CreateTempFile() tempFile := core.CreateTempFile()
defer func(name string) { defer func(name string) {
_ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed _ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed
}(tempFile.Name()) }(tempFile.Name())
// fetch the user's text editor // fetch the user's text editor
editor := backend.ParseConfig([][2]string{{"MUTN", "textEditor"}}, "")[0] editor := core.ParseConfig([][2]string{{"MUTN", "textEditor"}}, "")[0]
// write baseNote to tempFile (if it is not empty) // write baseNote to tempFile (if it is not empty)
if len(baseNote) > 0 { if len(baseNote) > 0 {
@@ -94,13 +94,13 @@ func editNote(baseNote []string) ([]string, bool) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
panic(backend.AnsiError + "Failed to write note with " + editor + backend.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement panic(core.AnsiError + "Failed to write note with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
} }
// open the tempFile for reading // open the tempFile for reading
tempFile, err = os.Open(tempFile.Name()) tempFile, err = os.Open(tempFile.Name())
if err != nil { if err != nil {
panic(backend.AnsiError + "Failed to read note written with " + editor + backend.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement panic(core.AnsiError + "Failed to read note written with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
} }
// read the edited note from the tempFile // read the edited note from the tempFile
@@ -114,7 +114,7 @@ func editNote(baseNote []string) ([]string, bool) {
_ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed _ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed
// remove trailing empty strings from the edited note // remove trailing empty strings from the edited note
note = backend.RemoveTrailingEmptyStrings(note) note = core.RemoveTrailingEmptyStrings(note)
// trim trailing whitespace from each note line // trim trailing whitespace from each note line
for i, line := range note { for i, line := range note {
+8 -7
View File
@@ -2,10 +2,11 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/rwinkhart/MUTN/src/backend"
"github.com/rwinkhart/MUTN/src/sync"
"os" "os"
"strings" "strings"
"github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync"
) )
// global constants used only in this file // global constants used only in this file
@@ -71,7 +72,7 @@ func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlter
} }
// print fileEntryName to screen // print fileEntryName to screen
fmt.Printf("%s%s%s ", colorCode, fileEntryName, backend.AnsiReset) fmt.Printf("%s%s%s ", colorCode, fileEntryName, core.AnsiReset)
return charCounter, colorAlternator return charCounter, colorAlternator
} }
@@ -81,7 +82,7 @@ func EntryListGen() {
fileList, dirList := sync.WalkEntryDir() fileList, dirList := sync.WalkEntryDir()
// print header bar w/total entry count // print header bar w/total entry count
fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+backend.AnsiReset) fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+core.AnsiReset)
// dirList iteration // dirList iteration
dirListLength := len(dirList) // save length for multiple references below dirListLength := len(dirList) // save length for multiple references below
@@ -127,7 +128,7 @@ func EntryListGen() {
containsFiles = true containsFiles = true
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped 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 indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+backend.AnsiReset+"\n", vanityDirectory) fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory)
} }
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator) charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator)
@@ -139,8 +140,8 @@ func EntryListGen() {
if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning 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 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 indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+backend.AnsiReset+"\n", vanityDirectory) fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory)
fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + backend.AnsiReset) fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + core.AnsiReset)
} else { // warn if the only thing that exists is the root-level directory } 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\".") fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
} }
+11 -11
View File
@@ -5,8 +5,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/MUTN/src/sync" "github.com/rwinkhart/libmutton/sync"
) )
const ansiShownPassword = "\033[38;5;10m" const ansiShownPassword = "\033[38;5;10m"
@@ -21,33 +21,33 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
// if the first field (password) is not empty, print it // if the first field (password) is not empty, print it
if decryptedEntry[0] != "" { if decryptedEntry[0] != "" {
if !hideSecrets { if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "Password:" + backend.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + backend.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + core.AnsiReset + "\n\n")
} else { } else {
fmt.Print(ansiDirectoryHeader + "Password:" + backend.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + backend.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n")
} }
} }
case 1: case 1:
// if the second field (username) is not empty, print it // if the second field (username) is not empty, print it
if decryptedEntry[1] != "" { if decryptedEntry[1] != "" {
fmt.Print(ansiDirectoryHeader + "Username:" + backend.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") fmt.Print(ansiDirectoryHeader + "Username:" + core.AnsiReset + "\n" + decryptedEntry[1] + "\n\n")
} }
case 2: case 2:
// if the third field (TOTP secret) is not empty, print it // if the third field (TOTP secret) is not empty, print it
if decryptedEntry[2] != "" { if decryptedEntry[2] != "" {
if !hideSecrets { if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + backend.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + backend.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + core.AnsiReset + "\n\n")
} else { } else {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + backend.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + backend.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n")
} }
} }
case 3: case 3:
// if the fourth field (url) is not empty, print it // if the fourth field (url) is not empty, print it
if decryptedEntry[3] != "" { if decryptedEntry[3] != "" {
fmt.Print(ansiDirectoryHeader + "URL:" + backend.AnsiReset + "\n" + decryptedEntry[3] + "\n\n") fmt.Print(ansiDirectoryHeader + "URL:" + core.AnsiReset + "\n" + decryptedEntry[3] + "\n\n")
} }
case 4: case 4:
// print the notes header // print the notes header
fmt.Println(ansiDirectoryHeader + "Notes:" + backend.AnsiReset) fmt.Println(ansiDirectoryHeader + "Notes:" + core.AnsiReset)
// combine remaining fields into a single string (to support Markdown rendering) // combine remaining fields into a single string (to support Markdown rendering)
var notesSlice []string var notesSlice []string
@@ -73,8 +73,8 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts a GPG-encrypted file before sending it to EntryReader // EntryReaderDecrypt is a wrapper for EntryReader that first decrypts a GPG-encrypted file before sending it to EntryReader
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) { func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
if isFile, _ := backend.TargetIsFile(targetLocation, true, 2); isFile { if isFile, _ := core.TargetIsFile(targetLocation, true, 2); isFile {
EntryReader(backend.DecryptGPG(targetLocation), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified EntryReader(core.DecryptGPG(targetLocation), 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 // do not exit, as this is the job of EntryReader
} }
+14 -14
View File
@@ -6,8 +6,8 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/MUTN/src/sync" "github.com/rwinkhart/libmutton/sync"
) )
// TempInitCli initializes the MUTN environment based on user input (will be replaced with a TUI menu) // TempInitCli initializes the MUTN environment based on user input (will be replaced with a TUI menu)
@@ -15,13 +15,13 @@ func TempInitCli() {
// gpgID // gpgID
var gpgID string var gpgID string
if inputBinary("Auto-generate GPG key?") { if inputBinary("Auto-generate GPG key?") {
gpgID = backend.GpgKeyGen() gpgID = core.GpgKeyGen()
} else { } else {
// select GPG key from menu // select GPG key from menu
uidSlice := backend.GpgUIDListGen() uidSlice := core.GpgUIDListGen()
gpgIDInt := inputMenuGen("Select GPG key:", uidSlice) gpgIDInt := inputMenuGen("Select GPG key:", uidSlice)
if gpgIDInt == 0 { if gpgIDInt == 0 {
fmt.Println(backend.AnsiError + "No GPG keys found - please generate one" + backend.AnsiReset) fmt.Println(core.AnsiError + "No GPG keys found - please generate one" + core.AnsiReset)
os.Exit(1) os.Exit(1)
} }
gpgID = uidSlice[gpgIDInt-1] gpgID = uidSlice[gpgIDInt-1]
@@ -34,7 +34,7 @@ func TempInitCli() {
configSSH := inputBinary("Configure SSH settings (for synchronization)?") configSSH := inputBinary("Configure SSH settings (for synchronization)?")
if configSSH { if configSSH {
// necessary SSH info // necessary SSH info
fmt.Println(AnsiBold + "\nNote:" + backend.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + backend.PathSeparator + ".ssh" + backend.PathSeparator + "known_hosts file.") fmt.Println(AnsiBold + "\nNote:" + core.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts file.")
sshUser := input("Remote SSH username:") sshUser := input("Remote SSH username:")
sshPort := input("Remote SSH port:") sshPort := input("Remote SSH port:")
sshIP := input("Remote SSH IP/domain:") sshIP := input("Remote SSH IP/domain:")
@@ -43,32 +43,32 @@ func TempInitCli() {
var sshKey string var sshKey string
var sshKeyIsFile bool var sshKeyIsFile bool
for !sshKeyIsFile { for !sshKeyIsFile {
fallbackSSHKey := backend.Home + backend.PathSeparator + ".ssh" + backend.PathSeparator + "id_ed25519" fallbackSSHKey := core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "id_ed25519"
sshKey = cmp.Or(expandPathWithHome(input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey) sshKey = cmp.Or(expandPathWithHome(input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey)
sshKeyIsFile, _ = backend.TargetIsFile(sshKey, false, 0) sshKeyIsFile, _ = core.TargetIsFile(sshKey, false, 0)
if !sshKeyIsFile { if !sshKeyIsFile {
fmt.Println(backend.AnsiError + "SSH identity file not found: " + sshKey + backend.AnsiReset) fmt.Println(core.AnsiError + "SSH identity file not found: " + sshKey + core.AnsiReset)
} }
} }
sshKeyProtected := inputBinary("Is the identity file password-protected?") sshKeyProtected := inputBinary("Is the identity file password-protected?")
// initialize libmutton directories // initialize libmutton directories
backend.DirInit(false) core.DirInit(false)
// write config file (temporarily assigns sshEntryRoot and sshIsWindows to null to pass initial device ID registration) // write config file (temporarily assigns sshEntryRoot and sshIsWindows to null to pass initial device ID registration)
backend.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}, {"LIBMUTTON", "sshUser", sshUser}, {"LIBMUTTON", "sshIP", sshIP}, {"LIBMUTTON", "sshPort", sshPort}, {"LIBMUTTON", "sshKey", sshKey}, {"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)}, {"LIBMUTTON", "sshEntryRoot", "null"}, {"LIBMUTTON", "sshIsWindows", "null"}}, false) core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}, {"LIBMUTTON", "sshUser", sshUser}, {"LIBMUTTON", "sshIP", sshIP}, {"LIBMUTTON", "sshPort", sshPort}, {"LIBMUTTON", "sshKey", sshKey}, {"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)}, {"LIBMUTTON", "sshEntryRoot", "null"}, {"LIBMUTTON", "sshIsWindows", "null"}}, false)
// generate and register device ID // generate and register device ID
sshEntryRoot, sshIsWindows := sync.DeviceIDGen() sshEntryRoot, sshIsWindows := sync.DeviceIDGen()
// update config file with sshEntryRoot and sshIsWindows // update config file with sshEntryRoot and sshIsWindows
backend.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, true) core.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, true)
} else { } else {
// initialize libmutton directories // initialize libmutton directories
backend.DirInit(false) core.DirInit(false)
// write config file // write config file
backend.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}}, false) core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}}, false)
} }
} }
+30 -30
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/libmutton/core"
) )
// global constants used only in this file // global constants used only in this file
@@ -14,13 +14,13 @@ const (
) )
func HelpMain() { func HelpMain() {
fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024 Randall Winkhart\n" + backend.AnsiReset + ` fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024 Randall Winkhart\n" + core.AnsiReset + `
This software exists under the MIT license; you may redistribute it under certain conditions. 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. This program comes with absolutely no warranty; type "mutn version" for details.
` + AnsiBold + "Usage:" + backend.AnsiReset + ` mutn [/<entry name> [argument] [option]] | [argument] ` + AnsiBold + "Usage:" + core.AnsiReset + ` mutn [/<entry name> [argument] [option]] | [argument]
` + AnsiBold + "Arguments:" + backend.AnsiReset + ` ` + AnsiBold + "Arguments:" + core.AnsiReset + `
help Bring up this menu help Bring up this menu
version Display version and license information version Display version and license information
init Set up MUTN (generates libmutton.ini) init Set up MUTN (generates libmutton.ini)
@@ -31,7 +31,7 @@ This program comes with absolutely no warranty; type "mutn version" for details.
shear Delete an existing entry shear Delete an existing entry
sync Manually sync the entry directory sync Manually sync the entry directory
` + AnsiBold + "Options:" + backend.AnsiReset + ` ` + AnsiBold + "Options:" + core.AnsiReset + `
copy: copy:
password|-pw|<blank> Copy the password in an entry to your clipboard password|-pw|<blank> Copy the password in an entry to your clipboard
username|-u Copy the username in an entry to your clipboard username|-u Copy the username in an entry to your clipboard
@@ -52,17 +52,17 @@ This program comes with absolutely no warranty; type "mutn version" for details.
note|-n Add a note entry note|-n Add a note entry
folder|-f Add a new folder for entries folder|-f Add a new folder for entries
` + AnsiBold + "Tip 1:" + backend.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>" ` + AnsiBold + "Tip 1:" + core.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>"
` + AnsiBold + "Tip 2:" + backend.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries ` + AnsiBold + "Tip 2:" + core.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries
` + AnsiBold + "Tip 3:" + backend.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help ` + AnsiBold + "Tip 3:" + core.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
` + AnsiBold + "Tip 4:" + backend.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n") ` + AnsiBold + "Tip 4:" + core.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n")
os.Exit(0) os.Exit(0)
} }
func HelpAdd() { func HelpAdd() {
fmt.Print(AnsiBold + "\nUsage:" + backend.AnsiReset + ` mutn /<entry name> add <option> fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> add <option>
` + AnsiBold + "Options:" + backend.AnsiReset + ` ` + AnsiBold + "Options:" + core.AnsiReset + `
add: add:
password|-pw|<blank> Add a password entry password|-pw|<blank> Add a password entry
note|-n Add a note entry note|-n Add a note entry
@@ -71,9 +71,9 @@ func HelpAdd() {
} }
func HelpEdit() { func HelpEdit() {
fmt.Print(AnsiBold + "\nUsage:" + backend.AnsiReset + ` mutn /<entry name> edit <option> fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> edit <option>
` + AnsiBold + "Options:" + backend.AnsiReset + ` ` + AnsiBold + "Options:" + core.AnsiReset + `
edit: edit:
password|-pw|<blank> Change the password in an entry password|-pw|<blank> Change the password in an entry
username|-u Change the username in an entry username|-u Change the username in an entry
@@ -85,9 +85,9 @@ func HelpEdit() {
} }
func HelpCopy() { func HelpCopy() {
fmt.Print(AnsiBold + "\nUsage:" + backend.AnsiReset + ` mutn /<entry name> copy <option> fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> copy <option>
` + AnsiBold + "Options:" + backend.AnsiReset + ` ` + AnsiBold + "Options:" + core.AnsiReset + `
copy: copy:
password|-pw|<blank> Copy the password in an entry to your clipboard password|-pw|<blank> Copy the password in an entry to your clipboard
username|-u Copy the username in an entry to your clipboard username|-u Copy the username in an entry to your clipboard
@@ -98,18 +98,18 @@ func HelpCopy() {
} }
func HelpGen() { func HelpGen() {
fmt.Print(AnsiBold + "\nUsage:" + backend.AnsiReset + ` mutn /<entry name> gen [option] fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> gen [option]
` + AnsiBold + "Options:" + backend.AnsiReset + ` ` + AnsiBold + "Options:" + core.AnsiReset + `
gen: gen:
update|-u Generate a password for an existing entry update|-u Generate a password for an existing entry
` + AnsiBold + "Tip:" + backend.AnsiReset + " If no options are provided, a new password entry is generated\n\n") ` + AnsiBold + "Tip:" + core.AnsiReset + " If no options are provided, a new password entry is generated\n\n")
os.Exit(0) os.Exit(0)
} }
func MITLicense() { func MITLicense() {
fmt.Print(AnsiBold + "\n MIT License" + backend.AnsiReset + ` fmt.Print(AnsiBold + "\n MIT License" + core.AnsiReset + `
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated
@@ -141,18 +141,18 @@ func Version() {
MITLicense() MITLicense()
fmt.Print("\n\n MUTN is a simple, self-hosted,\n SSH-synchronized password manager based on libmutton\n\n" + fmt.Print("\n\n MUTN is a simple, self-hosted,\n SSH-synchronized password manager based on libmutton\n\n" +
" .. ..\n" + " .. ..\n" +
" /()\\''.''. " + ansiVersionMeat + "♥♥♥♥" + backend.AnsiReset + " .''.''/()\\ _)\n" + " /()\\''.''. " + ansiVersionMeat + "♥♥♥♥" + core.AnsiReset + " .''.''/()\\ _)\n" +
" _. : * " + ansiVersionMeat + "♥♥♥♥♥♥ ♥♥♥♥♥♥♥♥" + backend.AnsiReset + " * : <[◎]|_|=\n" + " _. : * " + ansiVersionMeat + "♥♥♥♥♥♥ ♥♥♥♥♥♥♥♥" + core.AnsiReset + " * : <[◎]|_|=\n" +
" }-}-*] `..'..' " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥♥♥♥♥" + backend.AnsiReset + " `..'..' |\n" + " }-}-*] `..'..' " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥♥♥♥♥" + core.AnsiReset + " `..'..' |\n" +
" ◎-◎ // \\\\ " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥" + backend.AnsiReset + " // \\\\ /|\\\n" + " ◎-◎ // \\\\ " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥" + core.AnsiReset + " // \\\\ /|\\\n" +
ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" +
"\\" + ansiBlackOnWhite + " " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " MUTN Version " + backend.LibmuttonVersion + " " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " MUTN Version " + core.LibmuttonVersion + " " + core.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " The Tripe Transmission Update " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " The Tripe Transmission Update " + core.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Copyright (c) 2024 Randall Winkhart " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " Copyright (c) 2024 Randall Winkhart " + core.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + backend.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" +
"<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + backend.AnsiReset + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + core.AnsiReset +
"\n For more information, see:\n\n" + "\n For more information, see:\n\n" +
" https://github.com/rwinkhart/MUTN\n" + " https://github.com/rwinkhart/MUTN\n" +
" https://github.com/rwinkhart/libmutton\n\n") " https://github.com/rwinkhart/libmutton\n\n")
+8 -8
View File
@@ -6,8 +6,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/libmutton/core"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
// input prompts the user for input and returns the input as a string // input prompts the user for input and returns the input as a string
@@ -21,7 +21,7 @@ func input(prompt string) string {
// inputHidden prompts the user for input and returns the input as a string, hiding the input from the terminal // inputHidden prompts the user for input and returns the input as a string, hiding the input from the terminal
func inputHidden(prompt string) string { func inputHidden(prompt string) string {
fmt.Print("\n" + prompt + " ") fmt.Print("\n" + prompt + " ")
byteInput, _ := terminal.ReadPassword(int(os.Stdin.Fd())) byteInput, _ := term.ReadPassword(int(os.Stdin.Fd()))
password := string(byteInput) password := string(byteInput)
fmt.Println() fmt.Println()
return password return password
@@ -66,19 +66,19 @@ func inputMenuGen(prompt string, options []string) int {
// writeEntryCLI writes an entry to targetLocation and previews it (errors if no data is supplied) // writeEntryCLI writes an entry to targetLocation and previews it (errors if no data is supplied)
func writeEntryCLI(targetLocation string, unencryptedEntry []string, hideSecrets, verifyEntryDoesNotExist bool) { func writeEntryCLI(targetLocation string, unencryptedEntry []string, hideSecrets, verifyEntryDoesNotExist bool) {
if backend.EntryIsNotEmpty(unencryptedEntry) { if core.EntryIsNotEmpty(unencryptedEntry) {
// write the entry to the target location // write the entry to the target location
backend.WriteEntry(targetLocation, unencryptedEntry, verifyEntryDoesNotExist) core.WriteEntry(targetLocation, unencryptedEntry, verifyEntryDoesNotExist)
// preview the entry // preview the entry
fmt.Println(AnsiBold + "\nEntry Preview:" + backend.AnsiReset) fmt.Println(AnsiBold + "\nEntry Preview:" + core.AnsiReset)
EntryReader(unencryptedEntry, hideSecrets, true) EntryReader(unencryptedEntry, hideSecrets, true)
} else { } else {
fmt.Println(backend.AnsiError + "No data supplied for entry" + backend.AnsiReset) fmt.Println(core.AnsiError + "No data supplied for entry" + core.AnsiReset)
os.Exit(1) os.Exit(1)
} }
} }
// expandPathWithHome, given a path (as a string) containing "~", returns the path with "~" expanded to the user's home directory // expandPathWithHome, given a path (as a string) containing "~", returns the path with "~" expanded to the user's home directory
func expandPathWithHome(path string) string { func expandPathWithHome(path string) string {
return strings.Replace(path, "~", backend.Home, 1) return strings.Replace(path, "~", core.Home, 1)
} }