Skip to content

Commit 01db690

Browse files
committed
feat: add compose management (up, down, stop, remove, etc) and enhance logs functionality
refactor: new Keymapings for new compose management
1 parent 05c4d53 commit 01db690

File tree

7 files changed

+422
-80
lines changed

7 files changed

+422
-80
lines changed

internal/docker/client.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,117 @@ func DoAction(action, containerID string) error {
422422
return cmd.Run()
423423
}
424424

425+
type ComposeCommand struct {
426+
Binary string
427+
SubCommand string
428+
}
429+
430+
func GetComposeCommand() ComposeCommand {
431+
if runtimeBin() == "docker" {
432+
433+
if path, err := exec.LookPath("docker"); err == nil {
434+
if err := exec.Command(path, "compose", "version").Run(); err == nil {
435+
return ComposeCommand{Binary: "docker", SubCommand: "compose"}
436+
}
437+
}
438+
if path, err := exec.LookPath("docker-compose"); err == nil {
439+
return ComposeCommand{Binary: path, SubCommand: ""}
440+
}
441+
442+
} else {
443+
if path, err := exec.LookPath("podman-compose"); err == nil {
444+
return ComposeCommand{Binary: path, SubCommand: ""}
445+
}
446+
447+
if path, err := exec.LookPath("podman"); err == nil {
448+
return ComposeCommand{Binary: path, SubCommand: "compose"}
449+
}
450+
}
451+
452+
return ComposeCommand{Binary: "docker", SubCommand: "compose"}
453+
}
454+
455+
func RunComposeAction(action, project, workingDir string) error {
456+
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
457+
defer cancel()
458+
459+
cmdConfig := GetComposeCommand()
460+
461+
var args []string
462+
if cmdConfig.SubCommand != "" {
463+
args = append(args, cmdConfig.SubCommand)
464+
}
465+
466+
if project != "" {
467+
args = append(args, "-p", project)
468+
}
469+
470+
args = append(args, action)
471+
472+
switch action {
473+
case "up":
474+
args = append(args, "-d")
475+
case "logs":
476+
args = append(args, "--tail", "20")
477+
}
478+
479+
cmd := exec.CommandContext(ctx, cmdConfig.Binary, args...)
480+
481+
if workingDir != "" {
482+
cmd.Dir = workingDir
483+
}
484+
485+
output, err := cmd.CombinedOutput()
486+
if err != nil {
487+
488+
return fmt.Errorf("compose error (%s %s): %v\nOutput: %s", cmdConfig.Binary, action, err, string(output))
489+
}
490+
491+
return nil
492+
}
493+
494+
// GetComposeLogs runs `compose logs` for a given project and returns the output lines
495+
func GetComposeLogs(project, workingDir string) ([]string, error) {
496+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
497+
defer cancel()
498+
499+
cmdConfig := GetComposeCommand()
500+
var args []string
501+
if cmdConfig.SubCommand != "" {
502+
args = append(args, cmdConfig.SubCommand)
503+
}
504+
if project != "" {
505+
args = append(args, "-p", project)
506+
}
507+
args = append(args, "logs", "--tail", "15")
508+
509+
cmd := exec.CommandContext(ctx, cmdConfig.Binary, args...)
510+
if workingDir != "" {
511+
cmd.Dir = workingDir
512+
}
513+
514+
output, err := cmd.CombinedOutput()
515+
if err != nil {
516+
// return output even on error to give the caller something to show
517+
lines := []string{}
518+
for _, l := range strings.Split(string(output), "\n") {
519+
if s := strings.TrimSpace(l); s != "" {
520+
lines = append(lines, s)
521+
}
522+
}
523+
return lines, fmt.Errorf("compose logs error: %v\nOutput: %s", err, string(output))
524+
}
525+
526+
lines := []string{}
527+
for _, l := range strings.Split(string(output), "\n") {
528+
if s := strings.TrimSpace(l); s != "" {
529+
lines = append(lines, s)
530+
}
531+
}
532+
533+
return lines, nil
534+
}
535+
425536
// FetchComposeProjects fetches all Docker/Podman Compose projects with their containers
426537

427538
func FetchComposeProjects() (map[string]*ComposeProject, error) {

internal/tui/cmds.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,24 @@ func doAction(action, containerID string) tea.Cmd {
4545
}
4646
}
4747

48+
func composeActionCmd(action, project, workingDir string) tea.Cmd {
49+
return func() tea.Msg {
50+
err := docker.RunComposeAction(action, project, workingDir)
51+
return actionDoneMsg{err: err}
52+
}
53+
}
54+
4855
// fetch logs for a container
4956
func fetchLogsCmd(id string) tea.Cmd {
5057
return func() tea.Msg {
5158
lines, err := docker.GetLogs(id)
5259
return docker.LogsMsg{ID: id, Lines: lines, Err: err}
5360
}
5461
}
62+
63+
func fetchComposeLogsCmd(project, workingDir string) tea.Cmd {
64+
return func() tea.Msg {
65+
lines, err := docker.GetComposeLogs(project, workingDir)
66+
return docker.LogsMsg{ID: project, Lines: lines, Err: err}
67+
}
68+
}

internal/tui/compose-view.go

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func (m model) renderTreeRow(row treeRow, selected bool, idW, nameW, memoryW, cp
103103

104104
// Project row style
105105
projectStyle := lipgloss.NewStyle().Bold(true).Foreground(accent)
106+
if selected {
107+
return selectedStyle.Render(projectLabel)
108+
}
106109
return projectStyle.Render(projectLabel)
107110
}
108111

@@ -244,21 +247,8 @@ func (m *model) moveCursorUpTree() {
244247
m.cursor = 0
245248
return
246249
}
247-
i := m.cursor - 1
248-
for i >= 0 && m.flatList[i].isProject {
249-
i--
250-
}
251-
if i >= 0 {
252-
m.cursor = i
253-
} else {
254-
// clamp to first non-project if any
255-
for j := 0; j < len(m.flatList); j++ {
256-
if !m.flatList[j].isProject {
257-
m.cursor = j
258-
return
259-
}
260-
}
261-
m.cursor = 0
250+
if m.cursor > 0 {
251+
m.cursor--
262252
}
263253
}
264254

@@ -267,21 +257,8 @@ func (m *model) moveCursorDownTree() {
267257
m.cursor = 0
268258
return
269259
}
270-
i := m.cursor + 1
271-
for i < len(m.flatList) && m.flatList[i].isProject {
272-
i++
273-
}
274-
if i < len(m.flatList) {
275-
m.cursor = i
276-
} else {
277-
// clamp to last non-project if any
278-
for j := len(m.flatList) - 1; j >= 0; j-- {
279-
if !m.flatList[j].isProject {
280-
m.cursor = j
281-
return
282-
}
283-
}
284-
m.cursor = len(m.flatList) - 1
260+
if m.cursor < len(m.flatList)-1 {
261+
m.cursor++
285262
}
286263
}
287264

@@ -330,3 +307,35 @@ func (m *model) refreshInfoContainer() {
330307
}
331308
}
332309
}
310+
311+
func (m *model) getSelectedProject() (string, string) {
312+
if !m.composeViewMode || len(m.flatList) == 0 {
313+
return "", ""
314+
}
315+
if m.cursor >= len(m.flatList) {
316+
return "", ""
317+
}
318+
row := m.flatList[m.cursor]
319+
projectName := row.projectName
320+
if !row.isProject {
321+
if row.container != nil {
322+
projectName = row.container.ComposeProject
323+
}
324+
}
325+
326+
if projectName == "" || projectName == "Standalone Containers" {
327+
return "", ""
328+
}
329+
330+
if proj, ok := m.projects[projectName]; ok {
331+
return projectName, proj.WorkingDir
332+
}
333+
return projectName, ""
334+
}
335+
336+
func (m *model) isProjectSelected() bool {
337+
if !m.composeViewMode || len(m.flatList) == 0 || m.cursor >= len(m.flatList) {
338+
return false
339+
}
340+
return m.flatList[m.cursor].isProject
341+
}

internal/tui/help.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ func getHelpItems(m model) []list.Item {
2323
item{"R", "Restart selected container"},
2424
item{"D", "Remove selected container"},
2525
item{"E", fmt.Sprintf("Open interactive shell (%s)", m.settings.Shell)},
26-
item{"L", "View/Toggle container logs"},
26+
item{"L", "View/Toggle logs (container or compose project)"},
2727
item{"I", "View/Toggle container info"},
28+
item{"U", "Compose: up / start project"},
29+
item{"D", "Compose: down / stop project"},
30+
item{"R", "Compose: restart project"},
31+
item{"P", "Compose: pause/unpause project"},
32+
item{"X", "Compose: stop all containers in project"},
2833
item{"C", "Toggle compose/normal view"},
2934
item{"F2", "Open settings"},
3035
item{"F1", "Show this help"},

internal/tui/keymap.go

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,49 @@ import (
99
// ============================================================================
1010

1111
type keyMap struct {
12-
Up key.Binding
13-
Down key.Binding
14-
Start key.Binding
15-
Stop key.Binding
16-
Restart key.Binding
17-
Logs key.Binding
18-
Info key.Binding
19-
Exec key.Binding
20-
Remove key.Binding
21-
Refresh key.Binding
22-
PageUp key.Binding
23-
NextPage key.Binding
24-
PrevPage key.Binding
25-
PageDown key.Binding
26-
Quit key.Binding
27-
Help key.Binding
12+
Up key.Binding
13+
Down key.Binding
14+
Start key.Binding
15+
Stop key.Binding
16+
Restart key.Binding
17+
Logs key.Binding
18+
Info key.Binding
19+
Exec key.Binding
20+
Remove key.Binding
21+
Refresh key.Binding
22+
PageUp key.Binding
23+
NextPage key.Binding
24+
PrevPage key.Binding
25+
PageDown key.Binding
26+
Quit key.Binding
27+
Help key.Binding
28+
ComposeUp key.Binding
29+
ComposeDown key.Binding
30+
ComposeRestart key.Binding
31+
ComposePause key.Binding
32+
ComposeStop key.Binding
2833
}
2934

3035
var Keys = keyMap{
31-
Up: key.NewBinding(key.WithKeys("up", "k")),
32-
Down: key.NewBinding(key.WithKeys("down", "j")),
33-
Start: key.NewBinding(key.WithKeys("s", "S")),
34-
Stop: key.NewBinding(key.WithKeys("x", "X")),
35-
Logs: key.NewBinding(key.WithKeys("l", "L")),
36-
Info: key.NewBinding(key.WithKeys("i", "I")),
37-
Exec: key.NewBinding(key.WithKeys("e", "E")),
38-
Restart: key.NewBinding(key.WithKeys("r", "R")),
39-
Remove: key.NewBinding(key.WithKeys("d", "D")),
40-
Refresh: key.NewBinding(key.WithKeys("f5")),
41-
PageUp: key.NewBinding(key.WithKeys("pgup", "left")),
42-
NextPage: key.NewBinding(key.WithKeys("n", "pagedown")),
43-
PrevPage: key.NewBinding(key.WithKeys("p", "pageup")),
44-
PageDown: key.NewBinding(key.WithKeys("pgdown", "right")),
45-
Quit: key.NewBinding(key.WithKeys("q", "Q", "ctrl+c", "f10")),
46-
Help: key.NewBinding(key.WithKeys("f1", "?")),
36+
Up: key.NewBinding(key.WithKeys("up", "k")),
37+
Down: key.NewBinding(key.WithKeys("down", "j")),
38+
Start: key.NewBinding(key.WithKeys("s", "S")),
39+
Stop: key.NewBinding(key.WithKeys("x", "X")),
40+
Logs: key.NewBinding(key.WithKeys("l")),
41+
Info: key.NewBinding(key.WithKeys("i", "I")),
42+
Exec: key.NewBinding(key.WithKeys("e", "E")),
43+
Restart: key.NewBinding(key.WithKeys("r", "R")),
44+
Remove: key.NewBinding(key.WithKeys("d", "D")),
45+
Refresh: key.NewBinding(key.WithKeys("f5")),
46+
PageUp: key.NewBinding(key.WithKeys("pgup", "left")),
47+
NextPage: key.NewBinding(key.WithKeys("n", "pagedown")),
48+
PrevPage: key.NewBinding(key.WithKeys("p", "pageup")),
49+
PageDown: key.NewBinding(key.WithKeys("pgdown", "right")),
50+
Quit: key.NewBinding(key.WithKeys("q", "Q", "ctrl+c", "f10")),
51+
Help: key.NewBinding(key.WithKeys("f1", "?")),
52+
ComposeUp: key.NewBinding(key.WithKeys("u", "U")),
53+
ComposeDown: key.NewBinding(key.WithKeys("d", "D")),
54+
ComposeRestart: key.NewBinding(key.WithKeys("r", "R")),
55+
ComposePause: key.NewBinding(key.WithKeys("p", "P")),
56+
ComposeStop: key.NewBinding(key.WithKeys("x", "X")),
4757
}

0 commit comments

Comments
 (0)