From ecbcb0132728cc18016819a214378b642d92278e Mon Sep 17 00:00:00 2001
From: Simon Ruderich <simon@ruderich.org>
Date: Tue, 18 May 2021 18:15:23 +0200
Subject: [PATCH] safcm: move sync_changes.go and term.go to frontend package

---
 cmd/safcm/sync.go                             | 12 +--
 cmd/safcm/sync_sync.go                        |  8 +-
 .../sync_changes.go => frontend/changes.go    | 93 ++++++++++---------
 .../changes_test.go                           | 57 +++++-------
 {cmd/safcm => frontend}/term.go               |  4 +-
 {cmd/safcm => frontend}/term_test.go          |  2 +-
 6 files changed, 89 insertions(+), 87 deletions(-)
 rename cmd/safcm/sync_changes.go => frontend/changes.go (76%)
 rename cmd/safcm/sync_changes_test.go => frontend/changes_test.go (96%)
 rename {cmd/safcm => frontend}/term.go (97%)
 rename {cmd/safcm => frontend}/term_test.go (99%)

diff --git a/cmd/safcm/sync.go b/cmd/safcm/sync.go
index 10edc68..f1877cf 100644
--- a/cmd/safcm/sync.go
+++ b/cmd/safcm/sync.go
@@ -214,11 +214,11 @@ are only available after the hosts were contacted.
 func logEvent(x frontend.Event, level safcm.LogLevel, isTTY bool, failed *bool) {
 	// We have multiple event sources so this is somewhat ugly.
 	var prefix, data string
-	var color Color
+	var color frontend.Color
 	if x.Error != nil {
 		prefix = "[error]"
 		data = x.Error.Error()
-		color = ColorRed
+		color = frontend.ColorRed
 		// We logged an error, tell the caller
 		*failed = true
 	} else if x.Log.Level != 0 {
@@ -237,7 +237,7 @@ func logEvent(x frontend.Event, level safcm.LogLevel, isTTY bool, failed *bool)
 			prefix = "[debug2]"
 		default:
 			prefix = fmt.Sprintf("[INVALID=%d]", x.Log.Level)
-			color = ColorRed
+			color = frontend.ColorRed
 		}
 		data = x.Log.Text
 	} else {
@@ -254,19 +254,19 @@ func logEvent(x frontend.Event, level safcm.LogLevel, isTTY bool, failed *bool)
 			x.ConnEvent.Data = "remote helper upload in progress"
 		default:
 			prefix = fmt.Sprintf("[INVALID=%d]", x.ConnEvent.Type)
-			color = ColorRed
+			color = frontend.ColorRed
 		}
 		data = x.ConnEvent.Data
 	}
 
 	host := x.Host.Name()
 	if color != 0 {
-		host = ColorString(isTTY, color, host)
+		host = frontend.ColorString(isTTY, color, host)
 	}
 	// Make sure to escape control characters to prevent terminal
 	// injection attacks
 	if !x.Escaped {
-		data = EscapeControlCharacters(isTTY, data)
+		data = frontend.EscapeControlCharacters(isTTY, data)
 	}
 	log.Printf("%-9s [%s] %s", prefix, host, data)
 }
diff --git a/cmd/safcm/sync_sync.go b/cmd/safcm/sync_sync.go
index 3f0c30b..7e4d225 100644
--- a/cmd/safcm/sync_sync.go
+++ b/cmd/safcm/sync_sync.go
@@ -26,6 +26,7 @@ import (
 
 	"ruderich.org/simon/safcm"
 	"ruderich.org/simon/safcm/cmd/safcm/config"
+	"ruderich.org/simon/safcm/frontend"
 	"ruderich.org/simon/safcm/rpc"
 )
 
@@ -44,7 +45,12 @@ func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
 	}
 
 	// Display changes
-	changes := s.formatChanges(resp)
+	c := frontend.Changes{
+		DryRun: s.config.DryRun,
+		Quiet:  s.config.Quiet,
+		IsTTY:  s.isTTY,
+	}
+	changes := c.FormatChanges(resp)
 	if changes != "" {
 		s.log(safcm.LogInfo, true, changes)
 	}
diff --git a/cmd/safcm/sync_changes.go b/frontend/changes.go
similarity index 76%
rename from cmd/safcm/sync_changes.go
rename to frontend/changes.go
index 2650bb4..351cdfa 100644
--- a/cmd/safcm/sync_changes.go
+++ b/frontend/changes.go
@@ -1,4 +1,4 @@
-// "sync" sub-command: format changes
+// Frontend: Format changes
 
 // Copyright (C) 2021  Simon Ruderich
 //
@@ -15,7 +15,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package main
+package frontend
 
 import (
 	"fmt"
@@ -30,23 +30,29 @@ import (
 // the remote helper is untrusted and must be either escaped with %q or by
 // calling EscapeControlCharacters().
 
-func (s *Sync) formatChanges(resp safcm.MsgSyncResp) string {
+type Changes struct {
+	DryRun bool
+	Quiet  bool
+	IsTTY  bool
+}
+
+func (c *Changes) FormatChanges(resp safcm.MsgSyncResp) string {
 	var changes []string
 	if len(resp.FileChanges) > 0 {
 		changes = append(changes,
-			s.formatFileChanges(resp.FileChanges))
+			c.FormatFileChanges(resp.FileChanges))
 	}
 	if len(resp.PackageChanges) > 0 {
 		changes = append(changes,
-			s.formatPackageChanges(resp.PackageChanges))
+			c.FormatPackageChanges(resp.PackageChanges))
 	}
 	if len(resp.ServiceChanges) > 0 {
 		changes = append(changes,
-			s.formatServiceChanges(resp.ServiceChanges))
+			c.FormatServiceChanges(resp.ServiceChanges))
 	}
 	if len(resp.CommandChanges) > 0 {
 		changes = append(changes,
-			s.formatCommandChanges(resp.CommandChanges))
+			c.FormatCommandChanges(resp.CommandChanges))
 	}
 	if len(changes) == 0 {
 		// Notify user that the host was synced successfully
@@ -64,30 +70,30 @@ func (s *Sync) formatChanges(resp safcm.MsgSyncResp) string {
 	return "\n" + x
 }
 
-func (s *Sync) formatFileChanges(changes []safcm.FileChange) string {
+func (c *Changes) FormatFileChanges(changes []safcm.FileChange) string {
 	var buf strings.Builder
-	if s.config.DryRun {
+	if c.DryRun {
 		fmt.Fprintf(&buf, "will change %d file(s): (dry-run)\n",
 			len(changes))
 	} else {
 		fmt.Fprintf(&buf, "changed %d file(s):\n", len(changes))
 	}
 	for _, x := range changes {
-		fmt.Fprintf(&buf, "%s:", s.formatTarget(x.Path))
+		fmt.Fprintf(&buf, "%s:", c.FormatTarget(x.Path))
 
 		var info []string
 		if x.Created {
 			info = append(info,
-				ColorString(s.isTTY, ColorGreen, "created"),
-				formatFileType(x.New),
-				formatFileUserGroup(x.New),
-				formatFilePerm(x.New),
+				ColorString(c.IsTTY, ColorGreen, "created"),
+				FormatFileType(x.New),
+				FormatFileUserGroup(x.New),
+				FormatFilePerm(x.New),
 			)
 		} else {
 			if x.Old.Mode.Type() != x.New.Mode.Type() {
 				info = append(info, fmt.Sprintf("%s -> %s",
-					formatFileType(x.Old),
-					formatFileType(x.New),
+					FormatFileType(x.Old),
+					FormatFileType(x.New),
 				))
 			}
 			if x.Old.User != x.New.User ||
@@ -95,15 +101,15 @@ func (s *Sync) formatFileChanges(changes []safcm.FileChange) string {
 				x.Old.Group != x.New.Group ||
 				x.Old.Gid != x.New.Gid {
 				info = append(info, fmt.Sprintf("%s -> %s",
-					formatFileUserGroup(x.Old),
-					formatFileUserGroup(x.New),
+					FormatFileUserGroup(x.Old),
+					FormatFileUserGroup(x.New),
 				))
 			}
 			if config.FileModeToFullPerm(x.Old.Mode) !=
 				config.FileModeToFullPerm(x.New.Mode) {
 				info = append(info, fmt.Sprintf("%s -> %s",
-					formatFilePerm(x.Old),
-					formatFilePerm(x.New),
+					FormatFilePerm(x.Old),
+					FormatFilePerm(x.New),
 				))
 			}
 		}
@@ -113,13 +119,14 @@ func (s *Sync) formatFileChanges(changes []safcm.FileChange) string {
 		}
 
 		if x.DataDiff != "" {
-			fmt.Fprintf(&buf, "\n%s", s.formatDiff(x.DataDiff))
+			fmt.Fprintf(&buf, "\n%s", c.FormatDiff(x.DataDiff))
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
-func formatFileType(info safcm.FileChangeInfo) string {
+
+func FormatFileType(info safcm.FileChangeInfo) string {
 	switch info.Mode.Type() {
 	case 0: // regular file
 		return "file"
@@ -131,18 +138,18 @@ func formatFileType(info safcm.FileChangeInfo) string {
 		return fmt.Sprintf("invalid type %v", info.Mode.Type())
 	}
 }
-func formatFileUserGroup(info safcm.FileChangeInfo) string {
+func FormatFileUserGroup(info safcm.FileChangeInfo) string {
 	return fmt.Sprintf("%s(%d) %s(%d)",
 		EscapeControlCharacters(false, info.User), info.Uid,
 		EscapeControlCharacters(false, info.Group), info.Gid)
 }
-func formatFilePerm(info safcm.FileChangeInfo) string {
+func FormatFilePerm(info safcm.FileChangeInfo) string {
 	return fmt.Sprintf("%#o", config.FileModeToFullPerm(info.Mode))
 }
 
-func (s *Sync) formatPackageChanges(changes []safcm.PackageChange) string {
+func (c *Changes) FormatPackageChanges(changes []safcm.PackageChange) string {
 	var buf strings.Builder
-	if s.config.DryRun {
+	if c.DryRun {
 		fmt.Fprintf(&buf, "will install %d package(s): (dry-run)\n",
 			len(changes))
 	} else {
@@ -150,14 +157,14 @@ func (s *Sync) formatPackageChanges(changes []safcm.PackageChange) string {
 	}
 	for _, x := range changes {
 		// TODO: indicate if installation failed
-		fmt.Fprintf(&buf, "%s\n", s.formatTarget(x.Name))
+		fmt.Fprintf(&buf, "%s\n", c.FormatTarget(x.Name))
 	}
 	return buf.String()
 }
 
-func (s *Sync) formatServiceChanges(changes []safcm.ServiceChange) string {
+func (c *Changes) FormatServiceChanges(changes []safcm.ServiceChange) string {
 	var buf strings.Builder
-	if s.config.DryRun {
+	if c.DryRun {
 		fmt.Fprintf(&buf, "will modify %d service(s): (dry-run)\n",
 			len(changes))
 	} else {
@@ -172,13 +179,13 @@ func (s *Sync) formatServiceChanges(changes []safcm.ServiceChange) string {
 			info = append(info, "enabled")
 		}
 		fmt.Fprintf(&buf, "%s: %s\n",
-			s.formatTarget(x.Name),
+			c.FormatTarget(x.Name),
 			strings.Join(info, ", "))
 	}
 	return buf.String()
 }
 
-func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
+func (c *Changes) FormatCommandChanges(changes []safcm.CommandChange) string {
 	const indent = "   > "
 
 	// Quiet hides all successful, non-trigger commands which produce no
@@ -188,7 +195,7 @@ func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
 	// Instead, quiet shows them only when they produce output (e.g.
 	// `ainsl`, `rm -v`) and thus modify the host's state.
 	var noOutput int
-	if s.config.Quiet {
+	if c.Quiet {
 		for _, x := range changes {
 			if x.Trigger == "" &&
 				x.Error == "" &&
@@ -199,18 +206,18 @@ func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
 	}
 
 	var buf strings.Builder
-	if s.config.DryRun {
+	if c.DryRun {
 		fmt.Fprintf(&buf, "will execute %d command(s)", len(changes))
 	} else {
 		fmt.Fprintf(&buf, "executed %d command(s)", len(changes))
 	}
-	if noOutput > 0 && !s.config.DryRun {
+	if noOutput > 0 && !c.DryRun {
 		fmt.Fprintf(&buf, ", %d with no output (hidden)", noOutput)
 	}
 	if noOutput != len(changes) {
 		fmt.Fprintf(&buf, ":")
 	}
-	if s.config.DryRun {
+	if c.DryRun {
 		fmt.Fprintf(&buf, " (dry-run)")
 	}
 	fmt.Fprintf(&buf, "\n")
@@ -220,7 +227,7 @@ func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
 			continue
 		}
 
-		fmt.Fprintf(&buf, "%s", s.formatTarget(x.Command))
+		fmt.Fprintf(&buf, "%s", c.FormatTarget(x.Command))
 		if x.Trigger != "" {
 			fmt.Fprintf(&buf, ", trigger for %q", x.Trigger)
 		}
@@ -231,34 +238,34 @@ func (s *Sync) formatCommandChanges(changes []safcm.CommandChange) string {
 			// TODO: truncate very large outputs?
 			x := indentBlock(x.Output, indent)
 			fmt.Fprintf(&buf, ":\n%s",
-				EscapeControlCharacters(s.isTTY, x))
+				EscapeControlCharacters(c.IsTTY, x))
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
 
-func (s *Sync) formatTarget(x string) string {
+func (c *Changes) FormatTarget(x string) string {
 	x = fmt.Sprintf("%q", x) // escape!
-	return ColorString(s.isTTY, ColorCyan, x)
+	return ColorString(c.IsTTY, ColorCyan, x)
 }
 
-func (s *Sync) formatDiff(diff string) string {
+func (c *Changes) FormatDiff(diff string) string {
 	const indent = "   "
 
 	diff = indentBlock(diff, indent)
 	// Never color diff content as we want to color the whole diff
 	diff = EscapeControlCharacters(false, diff)
-	if !s.isTTY {
+	if !c.IsTTY {
 		return diff
 	}
 
 	var res []string
 	for _, x := range strings.Split(diff, "\n") {
 		if strings.HasPrefix(x, indent+"+") {
-			x = ColorString(s.isTTY, ColorGreen, x)
+			x = ColorString(c.IsTTY, ColorGreen, x)
 		} else if strings.HasPrefix(x, indent+"-") {
-			x = ColorString(s.isTTY, ColorRed, x)
+			x = ColorString(c.IsTTY, ColorRed, x)
 		}
 		res = append(res, x)
 	}
diff --git a/cmd/safcm/sync_changes_test.go b/frontend/changes_test.go
similarity index 96%
rename from cmd/safcm/sync_changes_test.go
rename to frontend/changes_test.go
index 219373c..02a95c4 100644
--- a/cmd/safcm/sync_changes_test.go
+++ b/frontend/changes_test.go
@@ -13,14 +13,13 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package main
+package frontend
 
 import (
 	"io/fs"
 	"testing"
 
 	"ruderich.org/simon/safcm"
-	"ruderich.org/simon/safcm/cmd/safcm/config"
 	"ruderich.org/simon/safcm/testutil"
 )
 
@@ -157,15 +156,13 @@ func TestFormatChanges(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			s := &Sync{
-				config: &config.Config{
-					DryRun: tc.dryRun,
-					Quiet:  tc.quiet,
-				},
-				isTTY: tc.isTTY,
+			c := Changes{
+				DryRun: tc.dryRun,
+				Quiet:  tc.quiet,
+				IsTTY:  tc.isTTY,
 			}
 
-			res := s.formatChanges(tc.resp)
+			res := c.FormatChanges(tc.resp)
 			testutil.AssertEqual(t, "res", res, tc.exp)
 		})
 	}
@@ -656,14 +653,12 @@ func TestFormatFileChanges(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			s := &Sync{
-				config: &config.Config{
-					DryRun: tc.dryRun,
-				},
-				isTTY: tc.isTTY,
+			c := Changes{
+				DryRun: tc.dryRun,
+				IsTTY:  tc.isTTY,
 			}
 
-			res := s.formatFileChanges(tc.changes)
+			res := c.FormatFileChanges(tc.changes)
 			testutil.AssertEqual(t, "res", res, tc.exp)
 		})
 	}
@@ -773,14 +768,12 @@ func TestFormatPackageChanges(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			s := &Sync{
-				config: &config.Config{
-					DryRun: tc.dryRun,
-				},
-				isTTY: tc.isTTY,
+			c := Changes{
+				DryRun: tc.dryRun,
+				IsTTY:  tc.isTTY,
 			}
 
-			res := s.formatPackageChanges(tc.changes)
+			res := c.FormatPackageChanges(tc.changes)
 			testutil.AssertEqual(t, "res", res, tc.exp)
 		})
 	}
@@ -931,14 +924,12 @@ func TestFormatServiceChanges(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			s := &Sync{
-				config: &config.Config{
-					DryRun: tc.dryRun,
-				},
-				isTTY: tc.isTTY,
+			c := Changes{
+				DryRun: tc.dryRun,
+				IsTTY:  tc.isTTY,
 			}
 
-			res := s.formatServiceChanges(tc.changes)
+			res := c.FormatServiceChanges(tc.changes)
 			testutil.AssertEqual(t, "res", res, tc.exp)
 		})
 	}
@@ -1230,15 +1221,13 @@ func TestFormatCommandChanges(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			s := &Sync{
-				config: &config.Config{
-					DryRun: tc.dryRun,
-					Quiet:  tc.quiet,
-				},
-				isTTY: tc.isTTY,
+			c := Changes{
+				DryRun: tc.dryRun,
+				Quiet:  tc.quiet,
+				IsTTY:  tc.isTTY,
 			}
 
-			res := s.formatCommandChanges(tc.changes)
+			res := c.FormatCommandChanges(tc.changes)
 			testutil.AssertEqual(t, "res", res, tc.exp)
 		})
 	}
diff --git a/cmd/safcm/term.go b/frontend/term.go
similarity index 97%
rename from cmd/safcm/term.go
rename to frontend/term.go
index 47f7d62..f50f162 100644
--- a/cmd/safcm/term.go
+++ b/frontend/term.go
@@ -1,4 +1,4 @@
-// Functions for terminal output
+// Frontend: Functions for terminal output
 
 // Copyright (C) 2021  Simon Ruderich
 //
@@ -15,7 +15,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package main
+package frontend
 
 import (
 	"fmt"
diff --git a/cmd/safcm/term_test.go b/frontend/term_test.go
similarity index 99%
rename from cmd/safcm/term_test.go
rename to frontend/term_test.go
index 479d7e8..7569a99 100644
--- a/cmd/safcm/term_test.go
+++ b/frontend/term_test.go
@@ -13,7 +13,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package main
+package frontend
 
 import (
 	"testing"
-- 
2.49.0