Skip to content

Commit

Permalink
feat: TUI + server + CLI rewrite (#33)
Browse files Browse the repository at this point in the history
## Summary

- **New Features**
- Introduced `tgcom`, a command-line tool for commenting and
uncommenting code in various languages.
  - Added SSH server support for remote interactions with `tgcom`.

- **Enhancements**
- Improved terminal user interface (TUI) for file selection, mode
selection, and action selection.
- Functions for modifying files based on comments, with support for
dry-run mode and terminal emulation.

- **Bug Fixes**
  - Corrected directory navigation and file selection in the TUI.
  - Fixed validations for label inputs in the TUI.

- **Tests**
- Comprehensive tests added for file selection, label input, mode
selection, and server functionality.


---------

Co-authored-by: Filippo Trotter <[email protected]>
Co-authored-by: Puria Nafisi Azizi <[email protected]>
  • Loading branch information
3 people authored Jul 9, 2024
1 parent d5d46bf commit 4088ab0
Show file tree
Hide file tree
Showing 19 changed files with 1,973 additions and 435 deletions.
158 changes: 99 additions & 59 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,37 @@ import (
"fmt"
"log"
"os"
"os/exec"
"runtime"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/dyne/tgcom/utils/modfile"
"github.com/dyne/tgcom/utils/tui"
"github.com/dyne/tgcom/utils/tui/modelutils"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

/* In these variables we store the arguments passed to flags -f, -l, -d and -a */
var FileToRead string
var inputFlag modfile.Config

/*
rootCmd is the command tgcom. "Use" is the name of the command, "Short" is a brief description of the command, "Long
var (
FileToRead string
inputFlag modfile.Config
remotePath string
Tui bool
)

is a longer description of the command, Run is the action that must be executed when command tgcom is called"
*/
var rootCmd = &cobra.Command{
Use: "tgcom",
Short: "tgcom is tool that allows users to comment or uncomment pieces of code",
Short: "tgcom is a tool that allows users to comment or uncomment pieces of code",
Long: `tgcom is a CLI library written in Go that allows users to
comment or uncomment pieces of code. It supports many different
languages including Go, C, Java, Python, Bash, and many others...`,

comment or uncomment pieces of code. It supports many different
languages including Go, C, Java, Python, Bash, and many others...`,
Run: func(cmd *cobra.Command, args []string) {
if remotePath != "" {
executeRemoteCommand(remotePath)
return
}

if noFlagsGiven(cmd) {
customUsageFunc(cmd)
os.Exit(1)
Expand All @@ -36,22 +43,14 @@ var rootCmd = &cobra.Command{
},
}

/* the one command used to run the main function (set by default by cobra-cli) */
func Execute() {
err := rootCmd.Execute()
if err != nil {
fmt.Println("Error executing command:", err)
os.Exit(1)
}
}

/*
pass in this function the flags of the command tgcom. Flags can be Persistend (so that if tgcom has a sub-command, e.g.
subtgcom, the flag defined for tgcom can be used as flags of subtgcom) or local (so flags are usable only for tgcom command)
*/
func init() {

rootCmd.SetHelpFunc(customHelpFunc)
rootCmd.SetUsageFunc(customUsageFunc)

Expand All @@ -62,15 +61,24 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&inputFlag.StartLabel, "start-label", "s", "", "pass argument to start-label to modify lines after start-label")
rootCmd.PersistentFlags().StringVarP(&inputFlag.EndLabel, "end-label", "e", "", "pass argument to end-label to modify lines up to end-label")
rootCmd.PersistentFlags().StringVarP(&inputFlag.Lang, "language", "L", "", "pass argument to language to specify the language of the input code")
rootCmd.MarkFlagsRequiredTogether("start-label", "end-label")
rootCmd.MarkFlagsMutuallyExclusive("line", "start-label")
rootCmd.MarkFlagsMutuallyExclusive("line", "end-label")
rootCmd.MarkFlagsOneRequired("file", "language")
rootCmd.MarkFlagsMutuallyExclusive("file", "language")
rootCmd.PersistentFlags().StringVarP(&remotePath, "remote", "w", "", "pass remote user, host, and directory in the format user@host:/path/to/directory")
rootCmd.PersistentFlags().BoolVarP(&Tui, "tui", "t", false, "run the terminal user interface")
// Mark flags based on command name
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if cmd.Name() != "server" {
cmd.MarkFlagsRequiredTogether("start-label", "end-label")
cmd.MarkFlagsMutuallyExclusive("line", "start-label")
cmd.MarkFlagsMutuallyExclusive("line", "end-label")
cmd.MarkFlagsOneRequired("file", "language", "remote", "tui")
cmd.MarkFlagsMutuallyExclusive("file", "language")
}
return nil
}

// Register server command
rootCmd.AddCommand(serverCmd)
}

/* function to see if no flag is given */
func noFlagsGiven(cmd *cobra.Command) bool {
hasFlags := false
cmd.Flags().VisitAll(func(f *pflag.Flag) {
Expand All @@ -81,49 +89,70 @@ func noFlagsGiven(cmd *cobra.Command) bool {
return !hasFlags
}

// ReadFlags parses command line flags and applies them to modify files or display information accordingly.
func ReadFlags(cmd *cobra.Command) {
if strings.Contains(FileToRead, ",") {
if cmd.Flags().Changed("line") {
fmt.Println("Warning: when passed multiple file to flag -f don't use -l flag")
if Tui {
currentDir, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current working directory: %v\n", err)
os.Exit(1)
}
if cmd.Flags().Changed("start-label") && cmd.Flags().Changed("end-label") {
fileInfo := strings.Split(FileToRead, ",")
for i := 0; i < len(fileInfo); i++ {
inputFlag.Filename = fileInfo[i]
if err := modfile.ChangeFile(inputFlag); err != nil {
log.Fatal(err)
}

// Initialize your model with the current directory
model := tui.Model{
State: "FileSelection",
FilesSelector: modelutils.InitialModel(currentDir, 20),
}
clearScreen()
// Bubble Tea program
p := tea.NewProgram(model)

// Start the program
if _, err := p.Run(); err != nil {
os.Exit(1)
}
clearScreen()
} else {

if strings.Contains(FileToRead, ",") {
if cmd.Flags().Changed("line") {
fmt.Println("Warning: when passing multiple files to flag -f, don't use -l flag")
}
} else {
fileInfo := strings.Split(FileToRead, ",")
for i := 0; i < len(fileInfo); i++ {
if strings.Contains(fileInfo[i], ":") {
parts := strings.Split(fileInfo[i], ":")
if len(parts) != 2 {
log.Fatalf("invalid syntax. Use 'File:lines'")
}
inputFlag.Filename = parts[0]
inputFlag.LineNum = parts[1]
if cmd.Flags().Changed("start-label") && cmd.Flags().Changed("end-label") {
fileInfo := strings.Split(FileToRead, ",")
for i := 0; i < len(fileInfo); i++ {
inputFlag.Filename = fileInfo[i]
if err := modfile.ChangeFile(inputFlag); err != nil {
log.Fatal(err)
}

} else {
log.Fatalf("invalid syntax. Use 'File:lines'")
}
}
}
} else {
if cmd.Flags().Changed("line") || cmd.Flags().Changed("start-label") && cmd.Flags().Changed("end-label") {
inputFlag.Filename = FileToRead
if err := modfile.ChangeFile(inputFlag); err != nil {
log.Fatal(err)
} else {
fileInfo := strings.Split(FileToRead, ",")
for i := 0; i < len(fileInfo); i++ {
if strings.Contains(fileInfo[i], ":") {
parts := strings.Split(fileInfo[i], ":")
if len(parts) != 2 {
log.Fatalf("invalid syntax. Use 'File:lines'")
}
inputFlag.Filename = parts[0]
inputFlag.LineNum = parts[1]
if err := modfile.ChangeFile(inputFlag); err != nil {
log.Fatal(err)
}
} else {
log.Fatalf("invalid syntax. Use 'File:lines'")
}
}
}
} else {
log.Fatalf("Not specified what you want to modify: add -l flag or -s and -e flags")
if cmd.Flags().Changed("line") || cmd.Flags().Changed("start-label") && cmd.Flags().Changed("end-label") {
inputFlag.Filename = FileToRead
if err := modfile.ChangeFile(inputFlag); err != nil {
log.Fatal(err)
}
} else {
log.Fatalf("Not specified what you want to modify: add -l flag or -s and -e flags")
}
}

}
}

Expand Down Expand Up @@ -172,3 +201,14 @@ func customUsageFunc(cmd *cobra.Command) error {
})
return nil
}
func clearScreen() {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "cls")
default:
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
cmd.Run()
}
125 changes: 125 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package cmd

import (
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"

"github.com/creack/pty"
"github.com/dyne/tgcom/utils/server"
"github.com/spf13/cobra"
"golang.org/x/term"
)

// serverCmd represents the server command
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start the SSH server",
Long: `Start the SSH server that allows remote interactions with tgcom.`,
Run: func(cmd *cobra.Command, args []string) {
server.StartServer()
},
}

func init() {
// Register the server command
rootCmd.AddCommand(serverCmd)
}

func executeRemoteCommand(remotePath string) {
parts := strings.SplitN(remotePath, "@", 2)
if len(parts) != 2 {
fmt.Println("Invalid format. Usage: tgcom -w user@remote:/path/folder")
os.Exit(1)
}

userHost := parts[0]
pathParts := strings.SplitN(parts[1], ":", 2)
if len(pathParts) != 2 {
fmt.Println("Invalid format. Usage: tgcom -w user@remote:/path/folder")
os.Exit(1)
}

host := pathParts[0]
dir := pathParts[1]

sshCmd := "ssh"
sshArgs := []string{"-t", "-p", "2222", userHost + "@" + host, "tgcom", dir}

// Start SSH command with PTY
if err := startSSHWithPTY(sshCmd, sshArgs); err != nil {
log.Fatalf("Error starting SSH with PTY: %v", err)
}
}

func startSSHWithPTY(cmd string, args []string) error {
// Create SSH command
sshCommand := exec.Command(cmd, args...)

// Start PTY
ptmx, err := pty.Start(sshCommand)
if err != nil {
return fmt.Errorf("failed to start PTY: %w", err)
}
defer ptmx.Close()

// Set terminal attributes
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("failed to make terminal raw: %w", err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)

// Resize PTY to current terminal size
if err := resizePTY(ptmx); err != nil {
return fmt.Errorf("failed to resize PTY: %w", err)
}

// Forward input to PTY
go func() {
_, _ = io.Copy(ptmx, os.Stdin)
}()

// Forward output from PTY
go func() {
_, _ = io.Copy(os.Stdout, ptmx)
}()

// Handle PTY signals and resize
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGWINCH)
for range ch {
if err := resizePTY(ptmx); err != nil {
log.Printf("Error resizing PTY: %v", err)
}
}
}()

// Wait for SSH command to finish
if err := sshCommand.Wait(); err != nil {
return fmt.Errorf("SSH command failed: %w", err)
}

// Wait a bit before exiting to ensure all output is processed
time.Sleep(100 * time.Millisecond)

return nil
}

func resizePTY(ptmx *os.File) error {
size, err := pty.GetsizeFull(os.Stdin)
if err != nil {
return fmt.Errorf("failed to get terminal size: %w", err)
}
if err := pty.Setsize(ptmx, size); err != nil {
return fmt.Errorf("failed to set terminal size: %w", err)
}
return nil
}
Loading

0 comments on commit 4088ab0

Please sign in to comment.