Skip to content

Commit 0979842

Browse files
feat: first commit
0 parents  commit 0979842

File tree

17 files changed

+2804
-0
lines changed

17 files changed

+2804
-0
lines changed

.github/copilot-instructions.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# LazyNginx AI Development Guide
2+
3+
## Project Overview
4+
LazyNginx is a terminal-based Nginx manager built with Go and Bubble Tea (TUI framework). It provides cross-platform menu-driven nginx management without requiring users to remember commands.
5+
6+
## Architecture
7+
8+
### Core Components
9+
- **main.go**: Entry point that initializes Bubble Tea program with `tea.WithAltScreen()` for full-screen TUI
10+
- **model.go**: Bubble Tea model implementing Update/View/Init pattern with two display modes:
11+
- `mode: "menu"` - Shows interactive menu with cursor navigation
12+
- `mode: "output"` - Displays command results (switched via Enter key)
13+
- **commands.go**: Command execution functions returning `tea.Msg` types for async operations
14+
- **messages.go**: Custom message types (`statusMsg`, `outputMsg`) for Bubble Tea's Elm architecture
15+
- **styles.go**: Lipgloss style definitions for consistent terminal UI theming
16+
17+
### Bubble Tea Pattern
18+
The app uses Bubble Tea's command pattern where:
19+
1. User selects menu item → `handleSelection()` returns a `tea.Cmd`
20+
2. Command executes asynchronously → returns a message (`statusMsg` or `outputMsg`)
21+
3. `Update()` receives message → updates model state
22+
4. `View()` renders based on current mode
23+
24+
## Platform-Specific Behavior
25+
26+
Commands try multiple approaches in sequence (returns first success):
27+
28+
**Start/Stop/Restart Operations:**
29+
1. Windows: `net start/stop nginx`
30+
2. Linux/systemd: `sudo systemctl [action] nginx`
31+
3. Direct: `sudo nginx [-s stop]`
32+
33+
**Status Check Cascading:**
34+
1. Windows: `tasklist /FI "IMAGENAME eq nginx.exe"`
35+
2. systemd: `systemctl is-active nginx`
36+
3. Unix: `pgrep -x nginx`
37+
4. Fallback: `ps aux | grep nginx`
38+
39+
**File Discovery:**
40+
Config/logs checked in order:
41+
- `/etc/nginx/` (Linux)
42+
- `C:\nginx\` (Windows)
43+
- `/usr/local/nginx/` (macOS/Unix)
44+
45+
## Development Patterns
46+
47+
### Adding New Commands
48+
1. Create function in `commands.go` returning `tea.Msg`:
49+
```go
50+
func myCommand() tea.Msg {
51+
output, err := exec.Command("mycommand").CombinedOutput()
52+
if err != nil {
53+
return outputMsg{output: "Error: " + string(output)}
54+
}
55+
return outputMsg{output: string(output)}
56+
}
57+
```
58+
59+
2. Add menu item to `initialModel()` choices slice in [model.go](model.go#L21-L32)
60+
61+
3. Map cursor position to command in `handleSelection()` switch statement in [model.go](model.go#L147-L163)
62+
63+
### Navigation Keys
64+
- Vim-style (`j`/`k`) and arrow keys handled in [model.go](model.go#L64-L78) Update() switch
65+
- `q` and `Ctrl+C` handled differently based on `mode` (quit vs return to menu)
66+
67+
### Styling Convention
68+
All UI styles use Lipgloss with hex color codes defined as package variables in [styles.go](styles.go). Status messages conditionally use `statusStyle` (green) or `errorStyle` (red) based on text content matching in [model.go](model.go#L108-L114).
69+
70+
## Building and Running
71+
72+
```bash
73+
# Build
74+
go build -o lazynginx
75+
76+
# Run (requires sudo for service operations on Linux/macOS)
77+
sudo ./lazynginx
78+
```
79+
80+
No tests exist. Manual testing requires nginx installed and verifying menu navigation + command execution on target OS.
81+
82+
## Dependencies
83+
- `github.com/charmbracelet/bubbletea` - TUI framework using Elm architecture
84+
- `github.com/charmbracelet/lipgloss` - Terminal styling (colors, padding, bold)
85+
86+
Binary output: `lazynginx` (Unix) or `lazynginx.exe` (Windows)

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Binaries
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
lazynginx
8+
lazynginx.exe
9+
10+
# Test binary
11+
*.test
12+
13+
# Output of the go coverage tool
14+
*.out
15+
16+
# Dependency directories
17+
vendor/
18+
19+
# Go workspace file
20+
go.work
21+
22+
# IDE
23+
.idea/
24+
.vscode/
25+
*.swp
26+
*.swo
27+
*~
28+
29+
# OS
30+
.DS_Store
31+
Thumbs.db
32+

README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# LazyNginx 🚀
2+
3+
A beautiful terminal-based Nginx manager built with Go and [Bubble Tea](https://github.com/charmbracelet/bubbletea).
4+
5+
## Features
6+
7+
- ✅ Check Nginx status
8+
- 🚀 Start/Stop/Restart Nginx
9+
- 🔄 Reload configuration
10+
- ✅ Test configuration
11+
- 📄 View configuration file
12+
- 📊 View error logs
13+
- 📈 View access logs
14+
- 🎨 Beautiful terminal UI
15+
16+
## Installation
17+
18+
### Prerequisites
19+
20+
- Go 1.21 or later
21+
- Nginx installed on your system
22+
23+
### Build from Source
24+
25+
```bash
26+
git clone <repository-url>
27+
cd lazynginx
28+
go mod download
29+
go build -o lazynginx
30+
```
31+
32+
### Run
33+
34+
```bash
35+
./lazynginx
36+
```
37+
38+
Or on Windows:
39+
```bash
40+
lazynginx.exe
41+
```
42+
43+
## Usage
44+
45+
### Navigation
46+
47+
- `` / `` or `k` / `j`: Navigate menu
48+
- `Enter`: Select option
49+
- `q` or `Ctrl+C`: Quit application
50+
51+
### Available Commands
52+
53+
1. **Check Status** - Check if Nginx is running
54+
2. **Start Nginx** - Start the Nginx service
55+
3. **Stop Nginx** - Stop the Nginx service
56+
4. **Restart Nginx** - Restart the Nginx service
57+
5. **Reload Configuration** - Reload Nginx configuration without downtime
58+
6. **Test Configuration** - Test Nginx configuration for syntax errors
59+
7. **View Configuration** - Display Nginx configuration file
60+
8. **View Error Logs** - Show last 50 lines of error log
61+
9. **View Access Logs** - Show last 50 lines of access log
62+
10. **Quit** - Exit the application
63+
64+
## Platform Support
65+
66+
The application automatically detects your platform and uses the appropriate commands:
67+
68+
- **Linux**: Uses `systemctl` when available, falls back to direct `nginx` commands
69+
- **Windows**: Uses `net start/stop` commands
70+
- **macOS/Unix**: Uses direct `nginx` commands
71+
72+
## Permissions
73+
74+
Some operations (start, stop, restart, reload) may require administrator/sudo privileges depending on your system configuration.
75+
76+
### Linux/macOS
77+
```bash
78+
sudo ./lazynginx
79+
```
80+
81+
### Windows
82+
Run as Administrator
83+
84+
## Configuration
85+
86+
The application automatically searches for Nginx in common locations:
87+
88+
- `/etc/nginx/nginx.conf` (Linux)
89+
- `C:\nginx\conf\nginx.conf` (Windows)
90+
- `/usr/local/nginx/conf/nginx.conf` (macOS/Unix)
91+
92+
## Logs
93+
94+
The application looks for logs in:
95+
96+
- `/var/log/nginx/` (Linux)
97+
- `C:\nginx\logs\` (Windows)
98+
- `/usr/local/nginx/logs/` (macOS/Unix)
99+
100+
## License
101+
102+
MIT
103+
104+
## Contributing
105+
106+
Contributions are welcome! Please feel free to submit a Pull Request.
107+

boxlayout/boxlayout.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package boxlayout
2+
3+
// Dimensions represents the coordinates of a box
4+
type Dimensions struct {
5+
X0 int
6+
X1 int
7+
Y0 int
8+
Y1 int
9+
}
10+
11+
// Direction defines how children are laid out
12+
type Direction int
13+
14+
const (
15+
ROW Direction = iota // Children stacked vertically
16+
COLUMN // Children arranged horizontally
17+
)
18+
19+
// Box represents a layout box that can contain children or a window
20+
type Box struct {
21+
// Direction decides how the children boxes are laid out
22+
Direction Direction
23+
24+
// Children are the sub-boxes
25+
Children []*Box
26+
27+
// Window refers to the name of the window this box represents
28+
Window string
29+
30+
// Size is static size (height for ROW parent, width for COLUMN parent)
31+
Size int
32+
33+
// Weight is dynamic size proportion (after static sizes allocated)
34+
Weight int
35+
}
36+
37+
// ArrangeWindows calculates the dimensions for all windows in the box tree
38+
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
39+
children := root.Children
40+
if len(children) == 0 {
41+
// leaf node
42+
if root.Window != "" {
43+
dimensionsForWindow := Dimensions{
44+
X0: x0,
45+
Y0: y0,
46+
X1: x0 + width - 1,
47+
Y1: y0 + height - 1,
48+
}
49+
return map[string]Dimensions{root.Window: dimensionsForWindow}
50+
}
51+
return map[string]Dimensions{}
52+
}
53+
54+
direction := root.Direction
55+
56+
var availableSize int
57+
if direction == COLUMN {
58+
availableSize = width
59+
} else {
60+
availableSize = height
61+
}
62+
63+
sizes := calcSizes(children, availableSize)
64+
65+
result := map[string]Dimensions{}
66+
offset := 0
67+
for i, child := range children {
68+
boxSize := sizes[i]
69+
70+
var resultForChild map[string]Dimensions
71+
if direction == COLUMN {
72+
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
73+
} else {
74+
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
75+
}
76+
77+
result = mergeDimensionMaps(result, resultForChild)
78+
offset += boxSize
79+
}
80+
81+
return result
82+
}
83+
84+
// calcSizes determines the size of each box based on static sizes and weights
85+
func calcSizes(boxes []*Box, availableSpace int) []int {
86+
totalWeight := 0
87+
reservedSpace := 0
88+
89+
for _, box := range boxes {
90+
if box.isStatic() {
91+
reservedSpace += box.Size
92+
} else {
93+
totalWeight += box.Weight
94+
}
95+
}
96+
97+
dynamicSpace := max(0, availableSpace-reservedSpace)
98+
99+
unitSize := 0
100+
extraSpace := 0
101+
if totalWeight > 0 {
102+
unitSize = dynamicSpace / totalWeight
103+
extraSpace = dynamicSpace % totalWeight
104+
}
105+
106+
result := make([]int, len(boxes))
107+
for i, box := range boxes {
108+
if box.isStatic() {
109+
result[i] = min(availableSpace, box.Size)
110+
} else {
111+
result[i] = unitSize * box.Weight
112+
}
113+
}
114+
115+
// Distribute remainder across dynamic boxes
116+
for i := 0; i < len(boxes) && extraSpace > 0; i++ {
117+
if !boxes[i].isStatic() {
118+
result[i]++
119+
extraSpace--
120+
}
121+
}
122+
123+
return result
124+
}
125+
126+
func (b *Box) isStatic() bool {
127+
return b.Size > 0
128+
}
129+
130+
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
131+
result := map[string]Dimensions{}
132+
for k, v := range a {
133+
result[k] = v
134+
}
135+
for k, v := range b {
136+
result[k] = v
137+
}
138+
return result
139+
}
140+
141+
func max(a, b int) int {
142+
if a > b {
143+
return a
144+
}
145+
return b
146+
}
147+
148+
func min(a, b int) int {
149+
if a < b {
150+
return a
151+
}
152+
return b
153+
}

0 commit comments

Comments
 (0)