TUI Support

This commit is contained in:
Berger Eugene 2022-05-02 01:49:19 +03:00
parent 296cfd0c71
commit 63c0e61eb4
10 changed files with 436 additions and 16 deletions

View File

@ -5,7 +5,7 @@ ifeq ($(OS),Windows_NT)
RM = cmd /C del /Q /F
endif
.PHONY: test run
.PHONY: test run testrace
buildrun: build run
@ -27,12 +27,16 @@ compile:
test:
go test -cover ./src/...
testrace:
go test -race ./src/...
coverhtml:
go test -coverprofile=coverage.out ./src
go tool cover -html=coverage.out
run:
./bin/${BINARY_NAME}${EXT}
PC_DEBUG_MODE=1 ./bin/${BINARY_NAME}${EXT}
clean:
$(RM) bin/${BINARY_NAME}*

8
go.mod
View File

@ -4,14 +4,17 @@ go 1.18
require (
github.com/fatih/color v1.13.0
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
github.com/gin-gonic/gin v1.7.7
github.com/joho/godotenv v1.4.0
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8
github.com/swaggo/swag v1.8.1
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
@ -24,12 +27,17 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect
golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.10 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

17
go.sum
View File

@ -13,6 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
@ -62,6 +66,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -72,6 +78,8 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
@ -87,6 +95,10 @@ github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
@ -134,6 +146,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -143,11 +156,15 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

View File

@ -15,6 +15,7 @@ type Project struct {
runningProcesses map[string]*Process
processStates map[string]*ProcessState
processLogs map[string]*pclog.ProcessLogBuffer
mapMutex sync.Mutex
logger pclog.PcLogger
wg sync.WaitGroup
@ -33,10 +34,12 @@ type ProcessConfig struct {
}
type ProcessState struct {
Name string `json:"name"`
Status string `json:"status"`
Restarts int `json:"restarts"`
ExitCode int `json:"exit_code"`
Name string `json:"name"`
Status string `json:"status"`
SystemTime string `json:"system_time"`
Restarts int `json:"restarts"`
ExitCode int `json:"exit_code"`
Pid int `json:"pid"`
}
func (p ProcessConfig) GetDependencies() []string {

View File

@ -26,10 +26,12 @@ type Process struct {
procColor func(a ...interface{}) string
noColor func(a ...interface{}) string
redColor func(a ...interface{}) string
logBuffer *pclog.ProcessLogBuffer
logger pclog.PcLogger
cmd *exec.Cmd
done bool
replica int
startTime time.Time
}
func NewProcess(
@ -37,6 +39,7 @@ func NewProcess(
logger pclog.PcLogger,
procConf ProcessConfig,
procState *ProcessState,
procLog *pclog.ProcessLogBuffer,
replica int) *Process {
colNumeric := rand.Intn(int(color.FgHiWhite)-int(color.FgHiBlack)) + int(color.FgHiBlack)
//logger, _ := zap.NewProduction()
@ -51,6 +54,7 @@ func NewProcess(
procState: procState,
done: false,
replica: replica,
logBuffer: procLog,
}
proc.procCond = *sync.NewCond(proc)
return proc
@ -65,7 +69,9 @@ func (p *Process) Run() error {
go p.handleOutput(stdout, p.handleInfo)
go p.handleOutput(stderr, p.handleError)
p.cmd.Start()
p.startTime = time.Now()
p.procState.Status = ProcessStateRunning
p.procState.Pid = p.cmd.Process.Pid
p.cmd.Wait()
p.Lock()
@ -154,6 +160,7 @@ func (p *Process) onProcessEnd() {
p.logger.Close()
}
p.procState.Status = ProcessStateCompleted
p.Lock()
p.done = true
p.Unlock()
@ -172,6 +179,25 @@ func (p *Process) getCommand() string {
return p.procConf.Command
}
func (p *Process) updateProcState() {
if p.procState.Status == ProcessStateRunning {
dur := time.Since(p.startTime)
p.procState.SystemTime = durationToString(dur)
}
}
func durationToString(dur time.Duration) string {
if dur.Minutes() < 3 {
return dur.Round(time.Second).String()
} else if dur.Minutes() < 60 {
return fmt.Sprintf("%.0fm", dur.Minutes())
} else if dur.Hours() < 24 {
return fmt.Sprintf("%dh%dm", int(dur.Hours()), int(dur.Minutes())%60)
} else {
return fmt.Sprintf("%dh", int(dur.Hours()))
}
}
func (p *Process) handleOutput(pipe io.ReadCloser,
handler func(message string)) {
@ -184,12 +210,15 @@ func (p *Process) handleOutput(pipe io.ReadCloser,
func (p *Process) handleInfo(message string) {
p.logger.Info(message, p.GetName(), p.replica)
fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), p.noColor(message))
fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), message)
p.logBuffer.Write(message)
}
func (p *Process) handleError(message string) {
p.logger.Error(message, p.GetName(), p.replica)
fmt.Printf("[%s]\t%s\n", p.procColor(p.GetNameWithReplica()), p.redColor(message))
p.logBuffer.Write(fmt.Sprintf("[deeppink]%s[-:-:-]", message))
}
func getRunnerShell() string {

67
src/app/process_test.go Normal file
View File

@ -0,0 +1,67 @@
package app
import (
"testing"
"time"
)
func TestDurationToString(t *testing.T) {
type args struct {
dur time.Duration
}
tests := []struct {
name string
args args
want string
}{
{
name: "milis",
args: args{
dur: 20 * time.Millisecond,
},
want: "0s",
},
{
name: "under 1m",
args: args{
dur: 20 * time.Second,
},
want: "20s",
},
{
name: "under 3m",
args: args{
dur: 150 * time.Second,
},
want: "2m30s",
},
{
name: "under 1h",
args: args{
dur: 30 * time.Minute,
},
want: "30m",
},
{
name: "under 24h",
args: args{
dur: 280 * time.Minute,
},
want: "4h40m",
},
{
name: "above 24h",
args: args{
dur: 25*time.Hour + 50*time.Minute,
},
want: "25h",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := durationToString(tt.args.dur); got != tt.want {
t.Errorf("DurationToString() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -20,6 +20,7 @@ var PROJ *Project
func (p *Project) Run() {
p.initProcessStates()
p.initProcessLogs()
p.runningProcesses = make(map[string]*Process)
runOrder := []ProcessConfig{}
p.WithProcesses([]string{}, func(process ProcessConfig) error {
@ -35,6 +36,7 @@ func (p *Project) Run() {
p.logger = pclog.NewLogger(p.LogLocation)
defer p.logger.Close()
}
//zerolog.SetGlobalLevel(zerolog.PanicLevel)
log.Debug().Msgf("Spinning up %d processes. Order: %q", len(runOrder), nameOrder)
for _, proc := range runOrder {
p.runProcess(proc)
@ -47,7 +49,11 @@ func (p *Project) runProcess(proc ProcessConfig) {
if isStringDefined(proc.LogLocation) {
procLogger = pclog.NewLogger(proc.LogLocation)
}
process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), 1)
procLog, err := p.getProcessLog(proc.Name)
if err != nil {
procLog = pclog.NewLogBuffer(1000)
}
process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), procLog, 1)
p.addRunningProcess(process)
p.wg.Add(1)
go func() {
@ -87,10 +93,12 @@ func (p *Project) initProcessStates() {
p.processStates = make(map[string]*ProcessState)
for key, proc := range p.Processes {
p.processStates[key] = &ProcessState{
Name: key,
Status: ProcessStatePending,
Restarts: 0,
ExitCode: 0,
Name: key,
Status: ProcessStatePending,
SystemTime: "",
Restarts: 0,
ExitCode: 0,
Pid: 0,
}
if proc.Disabled {
p.processStates[key].Status = ProcessStateDisabled
@ -98,10 +106,25 @@ func (p *Project) initProcessStates() {
}
}
func (p *Project) initProcessLogs() {
p.processLogs = make(map[string]*pclog.ProcessLogBuffer)
for key := range p.Processes {
p.processLogs[key] = pclog.NewLogBuffer(1000)
}
}
func (p *Project) GetProcessState(name string) *ProcessState {
if procState, ok := p.processStates[name]; ok {
proc := p.getRunningProcess(name)
if proc != nil {
proc.updateProcState()
} else {
procState.Pid = 0
procState.SystemTime = ""
}
return procState
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil
}
@ -153,6 +176,22 @@ func (p *Project) StopProcess(name string) error {
return nil
}
func (p *Project) getProcessLog(name string) (*pclog.ProcessLogBuffer, error) {
if procLogs, ok := p.processLogs[name]; ok {
return procLogs, nil
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil, fmt.Errorf("process %s doesn't exist", name)
}
func (p *Project) GetProcessLog(name string, offsetFromEnd, limit int) ([]string, error) {
logs, err := p.getProcessLog(name)
if err != nil {
return nil, err
}
return logs.GetLog(offsetFromEnd, limit), nil
}
func (p *Project) getProcesses(names ...string) ([]ProcessConfig, error) {
processes := []ProcessConfig{}
if len(names) == 0 {
@ -222,14 +261,14 @@ func (p *Project) GetDependenciesOrderNames() ([]string, error) {
return order, err
}
func (p *Project) GetLexicographicProcessNames() ([]string, error) {
func (p *Project) GetLexicographicProcessNames() []string {
names := []string{}
for name := range p.Processes {
names = append(names, name)
}
sort.Strings(names)
return names, nil
return names
}
func CreateProject(inputFile string) *Project {

View File

@ -10,6 +10,7 @@ import (
"github.com/f1bonacc1/process-compose/src/api"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/tui"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -23,7 +24,7 @@ func setupLogger() {
Out: os.Stdout,
TimeFormat: "06-01-02 15:04:05",
})
zerolog.SetGlobalLevel(zerolog.InfoLevel)
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
func isFlagPassed(name string) bool {
@ -40,11 +41,28 @@ func init() {
setupLogger()
}
func quiet() func() {
null, _ := os.Open(os.DevNull)
sout := os.Stdout
serr := os.Stderr
os.Stdout = null
os.Stderr = null
zerolog.SetGlobalLevel(zerolog.Disabled)
return func() {
defer null.Close()
os.Stdout = sout
os.Stderr = serr
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
func main() {
fileName := ""
port := 8080
isTui := true
flag.StringVar(&fileName, "f", app.DefaultFileNames[0], "path to file to load")
flag.IntVar(&port, "p", port, "port number")
flag.BoolVar(&isTui, "t", isTui, "disable tui (-t=false)")
flag.Parse()
if !isFlagPassed("f") {
pwd, err := os.Getwd()
@ -80,5 +98,13 @@ func main() {
go server.ListenAndServe()
project := app.CreateProject(fileName)
project.Run()
if isTui {
defer quiet()()
go project.Run()
tui.SetupTui()
} else {
project.Run()
}
}

View File

@ -0,0 +1,50 @@
package pclog
const (
slack = 100
)
type ProcessLogBuffer struct {
buffer []string
size int
}
func NewLogBuffer(size int) *ProcessLogBuffer {
return &ProcessLogBuffer{
size: size,
buffer: make([]string, 0, size),
}
}
func (b *ProcessLogBuffer) Write(message string) {
b.buffer = append(b.buffer, message)
if len(b.buffer) > b.size+slack {
b.buffer = b.buffer[slack:]
}
}
func (b ProcessLogBuffer) GetLog(offsetFromEnd, limit int) []string {
if len(b.buffer) == 0 {
return []string{}
}
if offsetFromEnd < 0 {
offsetFromEnd = 0
}
if offsetFromEnd > len(b.buffer) {
offsetFromEnd = len(b.buffer)
}
if limit < 1 {
limit = 0
}
if limit > len(b.buffer) {
limit = len(b.buffer)
}
if offsetFromEnd+limit > len(b.buffer) {
limit = len(b.buffer) - offsetFromEnd
}
if limit == 0 {
return b.buffer[len(b.buffer)-offsetFromEnd:]
}
return b.buffer[len(b.buffer)-offsetFromEnd : offsetFromEnd+limit]
}

177
src/tui/view.go Normal file
View File

@ -0,0 +1,177 @@
package tui
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type pcView struct {
procTable *tview.Table
statTable *tview.Table
appView *tview.Application
logsText *tview.TextView
statusText *tview.TextView
procNames []string
}
func newPcView() *pcView {
pv := &pcView{
appView: tview.NewApplication(),
logsText: tview.NewTextView().SetDynamicColors(true).SetScrollable(true),
statusText: tview.NewTextView().SetDynamicColors(true),
procNames: app.PROJ.GetLexicographicProcessNames(),
}
pv.procTable = pv.createProcTable()
pv.statTable = pv.createStatTable()
pv.appView.SetRoot(pv.createGrid(), true).EnableMouse(true).
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyF10:
pv.appView.Stop()
}
return event
})
if len(pv.procNames) > 0 {
pv.logsText.SetTitle(pv.procNames[0])
}
return pv
}
func (pv *pcView) fillTableData() {
if app.PROJ == nil {
return
}
for r, name := range pv.procNames {
state := app.PROJ.GetProcessState(name)
if state == nil {
return
}
pv.procTable.SetCell(r+1, 0, tview.NewTableCell(strconv.Itoa(state.Pid)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
pv.procTable.SetCell(r+1, 1, tview.NewTableCell(state.Name).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
pv.procTable.SetCell(r+1, 2, tview.NewTableCell(state.Status).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
pv.procTable.SetCell(r+1, 3, tview.NewTableCell(state.SystemTime).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
pv.procTable.SetCell(r+1, 4, tview.NewTableCell(strconv.Itoa(state.Restarts)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
pv.procTable.SetCell(r+1, 5, tview.NewTableCell(strconv.Itoa(state.ExitCode)).SetAlign(tview.AlignLeft).SetExpansion(1).SetTextColor(tcell.ColorLightSkyBlue))
}
}
func (pv pcView) getSelectedProcName() string {
if pv.procTable == nil {
return ""
}
row, _ := pv.procTable.GetSelection()
if row > 0 && row <= len(pv.procNames) {
return pv.procNames[row-1]
}
return ""
}
func (pv *pcView) fillLogs() {
name := pv.getSelectedProcName()
logs, err := app.PROJ.GetProcessLog(name, 1000, 0)
if err != nil {
pv.logsText.SetBorder(true).SetTitle(err.Error())
pv.logsText.Clear()
} else {
pv.logsText.SetText(strings.Join(logs, "\n"))
}
}
func (pv *pcView) onTableSelectionChange(row, column int) {
name := pv.getSelectedProcName()
pv.logsText.SetBorder(true).SetTitle(name)
}
func (pv *pcView) createProcTable() *tview.Table {
table := tview.NewTable().SetBorders(false).SetSelectable(true, false).SetSelectionChangedFunc(pv.onTableSelectionChange)
//pv.fillTableData()
table.Select(1, 1).SetFixed(1, 0).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyF9:
name := pv.getSelectedProcName()
app.PROJ.StopProcess(name)
case tcell.KeyF7:
name := pv.getSelectedProcName()
app.PROJ.StartProcess(name)
}
return event
})
columns := []string{
"PID", "NAME", "STATUS", "TIME", "RESTARTS", "EXIT CODE",
}
for i := 0; i < len(columns); i++ {
table.SetCell(0, i, tview.NewTableCell(columns[i]).
SetSelectable(false).SetExpansion(1))
}
return table
}
func (pv *pcView) createStatTable() *tview.Table {
table := tview.NewTable().SetBorders(false).SetSelectable(false, false)
table.SetCell(0, 0, tview.NewTableCell("Version:").SetSelectable(false).SetTextColor(tcell.ColorYellow))
table.SetCell(0, 1, tview.NewTableCell("v0.7.2").SetSelectable(false))
table.SetCell(1, 0, tview.NewTableCell("Hostname:").SetSelectable(false).SetTextColor(tcell.ColorYellow))
table.SetCell(1, 1, tview.NewTableCell("hhhppp").SetSelectable(false))
table.SetCell(2, 0, tview.NewTableCell("Processes:").SetSelectable(false).SetTextColor(tcell.ColorYellow))
table.SetCell(2, 1, tview.NewTableCell(strconv.Itoa(len(pv.procNames))).SetSelectable(false))
table.SetCell(0, 3, tview.NewTableCell("🔥 Process Compose").
SetSelectable(false).
SetAlign(tview.AlignRight).
SetExpansion(1).
SetTextColor(tcell.ColorYellow))
return table
}
func getHelpTextView() *tview.TextView {
textView := tview.NewTextView().
SetDynamicColors(true)
fmt.Fprintf(textView, "%s ", "F7[black:green]Start[-:-:-]")
fmt.Fprintf(textView, "%s ", "F9[black:green]Kill[-:-:-]")
fmt.Fprintf(textView, "%s ", "F10[black:green]Quit[-:-:-]")
return textView
}
func (pv pcView) createGrid() *tview.Grid {
grid := tview.NewGrid().
SetRows(3, 0, 0, 1).
//SetColumns(30, 0, 30).
SetBorders(true).
AddItem(pv.statTable, 0, 0, 1, 1, 0, 0, false).
AddItem(pv.procTable, 1, 0, 1, 1, 0, 0, true).
AddItem(pv.logsText, 2, 0, 1, 1, 0, 0, false).
AddItem(getHelpTextView(), 3, 0, 1, 1, 0, 0, false)
grid.SetTitle("Process Compose")
return grid
}
func (pv *pcView) updateTable() {
for {
time.Sleep(1000 * time.Millisecond)
pv.appView.QueueUpdateDraw(func() {
pv.fillTableData()
pv.fillLogs()
})
}
}
func SetupTui() {
pv := newPcView()
go pv.updateTable()
if err := pv.appView.Run(); err != nil {
panic(err)
}
}