Skip to content

Commit

Permalink
Merge pull request #352 from mutagen-io/status-line-tweaks
Browse files Browse the repository at this point in the history
cli: make a few additional formatting tweaks to list and monitor
  • Loading branch information
xenoscopic authored Jun 23, 2022
2 parents 56fa1b6 + 496875e commit 38f7d03
Show file tree
Hide file tree
Showing 21 changed files with 225 additions and 168 deletions.
2 changes: 1 addition & 1 deletion cmd/mutagen/daemon/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Connect(autostart, enforceVersionMatch bool) (*grpc.ClientConn, error) {

// Create a status line printer and defer a clear.
statusLinePrinter := &cmd.StatusLinePrinter{UseStandardError: true}
defer statusLinePrinter.BreakIfNonEmpty()
defer statusLinePrinter.BreakIfPopulated()

// Perform dialing in a loop until failure or success.
remainingPostAutostatAttempts := autostartRetryCount
Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/forward/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ func CreateWithSpecification(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return "", grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return "", fmt.Errorf("invalid create response received: %w", err)
}

Expand Down
10 changes: 6 additions & 4 deletions cmd/mutagen/forward/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ func monitorMain(_ *cobra.Command, arguments []string) error {
Selection: selection,
}

// If no template has been specified, then create a status line printer and
// defer a line break operation.
// If no template has been specified, then create a status line printer with
// bold text and defer a line break operation.
var statusLinePrinter *cmd.StatusLinePrinter
if template == nil {
statusLinePrinter = &cmd.StatusLinePrinter{}
defer statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter = &cmd.StatusLinePrinter{
Color: color.New(color.Bold),
}
defer statusLinePrinter.BreakIfPopulated()
}

// Track the last update time.
Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/forward/pause.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func PauseWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid pause response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/forward/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func ResumeWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid resume response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/forward/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func TerminateWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid terminate response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ func CreateWithSpecification(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return "", grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return "", fmt.Errorf("invalid create response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/flush.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ func FlushWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid flush response received: %w", err)
}

Expand Down
14 changes: 8 additions & 6 deletions cmd/mutagen/sync/list_monitor_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,14 @@ func printEndpoint(name string, url *url.URL, configuration *synchronization.Con
// Print connection status.
fmt.Println("\tConnected:", common.FormatConnectionStatus(state.Connected))

// Print content information.
fmt.Printf("\tContents: %s | %s | %s\n",
formatDirectoryCount(state.DirectoryCount),
formatFileCountAndSize(state.FileCount, state.TotalFileSize),
formatSymbolicLinkCount(state.SymbolicLinkCount),
)
// Print content information, if available.
if state.Scanned {
fmt.Printf("\tSynchronizable contents:\n\t\t%s\n\t\t%s\n\t\t%s\n",
formatDirectoryCount(state.DirectoryCount),
formatFileCountAndSize(state.FileCount, state.TotalFileSize),
formatSymbolicLinkCount(state.SymbolicLinkCount),
)
}

// Print scan problems, if any.
if len(state.ScanProblems) > 0 {
Expand Down
10 changes: 6 additions & 4 deletions cmd/mutagen/sync/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,14 @@ func monitorMain(_ *cobra.Command, arguments []string) error {
Selection: selection,
}

// If no template has been specified, then create a status line printer and
// defer a line break operation.
// If no template has been specified, then create a status line printer with
// bold text and defer a line break operation.
var statusLinePrinter *cmd.StatusLinePrinter
if template == nil {
statusLinePrinter = &cmd.StatusLinePrinter{}
defer statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter = &cmd.StatusLinePrinter{
Color: color.New(color.Bold),
}
defer statusLinePrinter.BreakIfPopulated()
}

// Track the last update time.
Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/pause.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func PauseWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid pause response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func ResetWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid reset response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ func ResumeWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid resume response received: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/mutagen/sync/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func TerminateWithSelection(
promptingCancel()
<-promptingErrors
if err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return grpcutil.PeelAwayRPCErrorLayer(err)
} else if err = response.EnsureValid(); err != nil {
statusLinePrinter.BreakIfNonEmpty()
statusLinePrinter.BreakIfPopulated()
return fmt.Errorf("invalid terminate response received: %w", err)
}

Expand Down
73 changes: 37 additions & 36 deletions cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,65 @@ type StatusLinePrinter struct {
// UseStandardError causes the printer to use standard error for its output
// instead of standard output (the default).
UseStandardError bool
// nonEmpty indicates whether or not the printer has printed any non-empty
// Color, if non-nil, will be used for colorizing output (if possible).
Color *color.Color
// populated indicates whether or not the printer has printed any non-empty
// content to the status line.
nonEmpty bool
populated bool
}

// Print prints a message to the status line, overwriting any existing content.
// Color escape sequences are supported. Messages will be truncated to a
// platform-dependent maximum length and padded appropriately.
func (p *StatusLinePrinter) Print(message string) {
// Determine output stream.
// Determine the output stream to use. We print to color-supporting output
// streams to ensure that color escape sequences are properly handled.
output := color.Output
if p.UseStandardError {
output = color.Error
}

// Print the message, prefixed with a carriage return to wipe out the
// previous line (if any). Ensure that the status prints as a specified
// width, truncating or right-padding with space as necessary. On POSIX
// systems, this width is 80 characters and on Windows it's 79. The reason
// for 79 on Windows is that for cmd.exe consoles the line width needs to be
// narrower than the console (which is 80 columns by default) for carriage
// return wipes to work (if it's the same width, the next carriage return
// overflows to the next line, behaving exactly like a newline). We print to
// the color output so that color escape sequences are properly handled - in
// all other cases this will behave just like standard output.
// TODO: We should probably try to detect the console width.
fmt.Fprintf(output, statusLineFormat, message)

// Update our non-empty status. We're always non-empty after printing
// because we print padding as well.
p.nonEmpty = true
// Print the message.
if p.Color != nil {
p.Color.Fprintf(output, statusLineFormat, message)
} else {
fmt.Fprintf(output, statusLineFormat, message)
}

// Update our populated status. The line is always populated in this case
// because even an empty message will be padded with spaces.
// TODO: We could possibly make this more precise, e.g. tracking whether or
// not message is empty or contains only spaces. In cases like these,
// BreakIfPopulated could potentially just return the cursor to the beginning
// of the line instead of printing a newline. But it's a bit unclear what
// the semantics of this should look like, what types of whitespace should
// be classified as empty, etc. For example, an empty line might be used as
// a visual delimiter, or a message could contain tabs and/or newlines.
p.populated = true
}

// Clear clears any content on the status line and moves the cursor back to the
// beginning of the line.
func (p *StatusLinePrinter) Clear() {
// Write over any existing data.
p.Print("")

// Determine output stream.
// Determine the output stream to use.
output := os.Stdout
if p.UseStandardError {
output = os.Stderr
}

// Wipe out any existing line.
fmt.Fprint(output, "\r")
// Wipe out any existing content and return the cursor to the beginning of
// the line.
fmt.Fprintf(output, statusLineClearFormat, "")

// Update our non-empty status.
p.nonEmpty = false
// Update our populated status.
p.populated = false
}

// BreakIfNonEmpty prints a newline character if the current line is non-empty.
func (p *StatusLinePrinter) BreakIfNonEmpty() {
// If the status line contents are non-empty, then print a newline and mark
// ourselves as empty.
if p.nonEmpty {
// Determine output stream.
// BreakIfPopulated prints a newline character if the current line is non-empty.
func (p *StatusLinePrinter) BreakIfPopulated() {
// Only perform an operation if the status line is populated with content.
if p.populated {
// Determine the output stream to use.
output := os.Stdout
if p.UseStandardError {
output = os.Stderr
Expand All @@ -81,8 +82,8 @@ func (p *StatusLinePrinter) BreakIfNonEmpty() {
// Print a line break.
fmt.Fprintln(output)

// Update our non-empty status.
p.nonEmpty = false
// Update our populated status.
p.populated = false
}
}

Expand Down Expand Up @@ -111,7 +112,7 @@ func (p *StatusLinePrompter) Prompt(message string) (string, error) {
//
// HACK: This is somewhat of a heuristic that relies on knowledge of how
// Mutagen's internal prompting/messaging works in practice.
p.Printer.BreakIfNonEmpty()
p.Printer.BreakIfPopulated()

// Perform command line prompting.
//
Expand Down
15 changes: 14 additions & 1 deletion cmd/output_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
package cmd

const (
// statusLineFormat is the format string to use for status line printing.
// statusLineFormat is the format string to use for status line printing. On
// POSIX systems, we truncate and pad messages (with spaces) so that the
// printed content is exactly 80 characters. This ensures that (a) all
// content from the previous line is overwritten, (b) the cursor is not
// flashing back and forth to different positions at the end of the printed
// content, and (c) that the content doesn't overflow the terminal. Of
// course, the last condition is contingent on the terminal being at least
// 80 characters wide, and newlines will occur if that's not the case, but
// 80 characters is a reasonable minimum based on the minimum width of a
// VT100 terminal.
statusLineFormat = "\r%-80.80s"
// statusLineClearFormat is the format string to use for printing an empty
// string to clear the status line. It adds a carriage return to return the
// cursor to the beginning of the line.
statusLineClearFormat = statusLineFormat + "\r"
)
18 changes: 17 additions & 1 deletion cmd/output_windows.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
package cmd

const (
// statusLineFormat is the format string to use for status line printing.
// statusLineFormat is the format string to use for status line printing. On
// Windows systems, we truncate and pad messages (with spaces) so that the
// printed content is exactly 79 characters. This ensures that (a) all
// content from the previous line is overwritten, (b) the cursor is not
// flashing back and forth to different positions at the end of the printed
// content, and (c) that the content doesn't overflow the terminal. Of
// course, the last condition is contingent on the terminal being at least
// 80 characters wide, and newlines will occur if that's not the case, but
// 80 characters is the default width of the console on most versions of
// Windows, and modern versions are even wider. The reason we have to limit
// ourselves to 79 characters of content instead of 80 is that carriage
// return wipes don't work if the cursor has already printed a character in
// the last position of the line on Windows.
statusLineFormat = "\r%-79.79s"
// statusLineClearFormat is the format string to use for printing an empty
// string to clear the status line. It adds a carriage return to return the
// cursor to the beginning of the line.
statusLineClearFormat = statusLineFormat + "\r"
)
5 changes: 5 additions & 0 deletions pkg/api/models/synchronization/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type EndpointState struct {
// Connected indicates whether or not the controller is currently connected
// to the endpoint.
Connected bool `json:"connected"`
// Scanned indicates whether or not at least one scan has been performed on
// the endpoint.
Scanned bool `json:"scanned"`
// DirectoryCount is the number of synchronizable directory entries
// contained in the last snapshot from the endpoint.
DirectoryCount uint64 `json:"directoryCount,omitempty"`
Expand Down Expand Up @@ -139,6 +142,7 @@ func (s *Session) loadFromInternal(state *synchronization.State) {
ExcludedConflicts: state.ExcludedConflicts,
AlphaState: EndpointState{
Connected: state.AlphaState.Connected,
Scanned: state.AlphaState.Scanned,
DirectoryCount: state.AlphaState.DirectoryCount,
FileCount: state.AlphaState.FileCount,
SymbolicLinkCount: state.AlphaState.SymbolicLinkCount,
Expand All @@ -151,6 +155,7 @@ func (s *Session) loadFromInternal(state *synchronization.State) {
},
BetaState: EndpointState{
Connected: state.BetaState.Connected,
Scanned: state.BetaState.Scanned,
DirectoryCount: state.BetaState.DirectoryCount,
FileCount: state.BetaState.FileCount,
SymbolicLinkCount: state.BetaState.SymbolicLinkCount,
Expand Down
Loading

0 comments on commit 38f7d03

Please sign in to comment.