Compare commits

...

34 Commits
v0.1 ... master

Author SHA1 Message Date
2ed3dc36f3 doc/CHANGELOG: update for v0.3.0 2025-01-06 19:32:59 +13:00
e6c10bac60 doc: add screenshot 2025-01-06 19:32:59 +13:00
5a627637f4 doc/README: update descriptions 2025-01-06 19:32:59 +13:00
697e66f9c4 ui: set treeviews uniform row heights, remove root indentation 2025-01-06 19:32:59 +13:00
eea2097b73 ui: delete dead code (the default sortable is sufficient) 2025-01-06 19:32:59 +13:00
5c5f2007f4 ui: preserve scroll changes that occur during docker refresh 2025-01-06 19:32:59 +13:00
6f8a984f3c ui: support --sudo to run all docker subcommands with sudo 2025-01-06 19:32:56 +13:00
c23090739a miqt: use index tab comparisons instead of pointer comparisons, fix context menu offset 2025-01-06 19:24:51 +13:00
8705777203 ui: use const defines for tab indexes 2025-01-06 19:24:51 +13:00
86730024f6 gitignore: update for miqt port 2025-01-06 19:24:51 +13:00
49a438bbb8 ui: delete dead code 2025-01-06 19:24:51 +13:00
4d0b2a7a8c miqt: fixes for string parsing of blank links in docker output 2025-01-06 19:24:51 +13:00
bc2216a9f8 miqt: remove QTimer::singleShot that does not seem to be needed 2025-01-06 19:24:51 +13:00
89f34d8621 ui: add keyboard accelerators 2025-01-06 19:24:51 +13:00
90fe6b862e miqt: port timers from Qt to native Go timers 2025-01-06 19:24:51 +13:00
dcc8edd3f8 miqt: initial port 2025-01-06 19:24:48 +13:00
e9c7c12450 miqt: clean up repo 2025-01-04 19:22:53 +13:00
Salem Yaslem
a1e3c7202e fix github action 2024-12-17 11:37:59 +03:00
Salem Yaslem
774a38d236 restore sessions to open container shell from flatpak application 2024-12-17 11:34:00 +03:00
Salem Yaslem
d56326304d remove dbus from flatpak 2024-12-17 10:39:19 +03:00
Salem Yaslem
549ca648df make all columns sortable 2024-10-01 01:50:23 +03:00
Salem Yaslem
1963d02b56 fix docker-cli permissions 2024-09-28 19:41:28 +03:00
Salem Yaslem
eb4f0a0568 fix docker-cli permissions 2024-09-28 19:38:38 +03:00
Salem Yaslem
813652e881 Merge branch 'master' of github.com:xlmnxp/Qocker 2024-09-28 19:35:28 +03:00
Salem Yaslem
e1543cc0e5 fix docker-cli 2024-09-28 19:35:20 +03:00
Salem Yaslem
66318e645e
Update build.yml 2024-09-28 19:30:39 +03:00
Salem Yaslem
311db6f32e
Update build.yml 2024-09-28 19:27:51 +03:00
Salem Yaslem
60b7425070 Merge branch 'master' of github.com:xlmnxp/Qocker 2024-09-28 19:21:41 +03:00
Salem Yaslem
78503982b8 prepare to flatpak for flathub publishing 2024-09-28 19:21:37 +03:00
Salem Yaslem
df9c97a4ab
Create build.yml 2024-09-28 19:03:12 +03:00
Salem Yaslem
cad5588b72 add flatpak support with docker and terminal 2024-09-28 19:01:32 +03:00
Salem Yaslem
13d16fac31
Merge pull request #12 from dreamyukii/flatpak-manifest
@dreamyukii thank you 🙏, merged
2024-09-28 18:24:08 +03:00
sekalengrengginang
5f7db80c51 fix: fix flatpak manifest 2024-09-28 20:47:45 +07:00
Salem Yaslem
cac5fe0e3d add(#10) flatpak packaging 2024-09-25 22:14:27 +03:00
10 changed files with 955 additions and 724 deletions

3
.gitignore vendored
View File

@ -1,2 +1 @@
/.idea
/venv
qocker-miqt

14
CHANGELOG.md Normal file
View File

@ -0,0 +1,14 @@
# CHANGELOG
## v0.3.0 2025-01-06
- Port Qocker to MIQT
- Add `--sudo` argument support
## v0.2 2024-09-26
- Upstream v0.2
## v0.1 2024-09-25
- Upstream v0.1

View File

@ -1,63 +1,17 @@
# Qocker
# Qocker-miqt
![Qocker Screenshot](./assets/screenshot.png)
Qocker-miqt is a user-friendly GUI application for managing Docker containers.
Qocker is a user-friendly GUI application for managing Docker containers. Built with PyQt5, it provides an intuitive interface for viewing and interacting with your Docker containers.
This is a fork of [Qocker](https://github.com/xlmnxp/Qocker) ported to the [MIQT](https://github.com/mappu/miqt) library for demonstration purposes.
## Features
## Building
- **Container Overview**: View all your Docker containers in a tree-like structure.
- **Quick Terminal Access**: Open a terminal for any container with a double-click.
- **Container Management**: Start, stop, and remove containers directly from the GUI.
- **Real-time Updates**: Container statuses are updated in real-time.
- **Cross-platform**: Works on Windows, macOS, and Linux.
## Installation
1. Ensure you have Python 3.6+ and Docker installed on your system.
2. Clone this repository:
```
git clone https://github.com/xlmnxp/qocker.git
```bash
apt install qt6-base-dev build-essential golang-go
go build -ldflags '-s -w'
./qocker-miqt
```
3. Navigate to the project directory:
```
cd qocker
```
4. Install the required dependencies:
```
pip install -r requirements.txt
```
## Usage
To start Qocker, run:
```
python3 main.py
```
- **View Containers**: All your Docker containers will be displayed in the main window.
- **Open Terminal**: Double-click on any container to open a terminal session for that container.
- **Manage Containers**: Use the buttons or context menu to start, stop, or remove containers.
## Requirements
- Python 3.6+
- PyQt5
- Docker
## Contributing
Contributions to Qocker are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE.md) file for details.
## Acknowledgments
- Thanks to the PyQt and Docker teams for their fantastic tools.
## Contact
If you have any questions, feel free to reach out to me at [email](mailto:s@sy.sa).
If your `docker` binary requires `sudo`, then
1. Run `sudo docker` once to prime the sudo login cache; then
2. Run `./qocker-miqt --sudo`. Then qocker will use sudo for all docker invocations.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

BIN
doc/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module code.ivysaur.me/qocker-miqt
go 1.19
require github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c // indirect

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/mappu/miqt v0.7.1 h1:CIegOqnF9sxSHs4eyqOgAHbuhFwCu1hth4b989ZTP1k=
github.com/mappu/miqt v0.7.1/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c h1:4oJzer4B//aJ8B/dCFF9yLPgSLobqXVhUpfQh/Tdc5U=
github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=

920
main.go Normal file
View File

@ -0,0 +1,920 @@
package main
import (
"os"
"os/exec"
"runtime"
"strings"
"time"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
)
const AutoRefreshInterval = 1 * time.Second
const (
ContainersTab int = 0
ImagesTab int = 1
NetworksTab int = 2
VolumesTab int = 3
)
var (
dockerSudo bool
)
func NewStatusDelegate(status string) *qt.QWidget {
mw := qt.NewQWidget2()
layout := qt.NewQHBoxLayout(mw)
layout.SetContentsMargins(4, 4, 4, 4)
layout.SetSpacing(8)
statusCircle := qt.NewQWidget2()
statusCircle.SetFixedSize2(12, 12)
var color string = "red"
if strings.Contains(status, "Up") {
color = "green"
}
statusCircle.SetStyleSheet("background-color: " + color + "; border-radius: 6px;")
statusLabel := qt.NewQLabel3(status)
layout.AddWidget(statusCircle)
layout.AddWidget(statusLabel.QWidget)
layout.AddStretch()
return mw
}
func dockerCommand(args ...string) *exec.Cmd {
if dockerSudo {
newargs := []string{"docker"}
newargs = append(newargs, args...)
return exec.Command("sudo", newargs...)
}
return dockerCommand(args...)
}
func openTerminal(container_id string) {
command := "docker exec -it " + container_id + " sh -c '[ -x /bin/bash ] && exec /bin/bash || exec /bin/sh'"
if dockerSudo {
command = "sudo " + command
}
popupCommand(command)
}
func openLogs(container_id string) {
command := "docker logs -f " + container_id
if dockerSudo {
command = "sudo " + command
}
popupCommand(command)
}
func popupCommand(command string) {
switch runtime.GOOS {
case "darwin":
exec.Command("open", "-a", "Terminal", "--", "sh", "-c", command).Start()
case "linux":
exec.Command("x-terminal-emulator", "-e", "sh -c \""+command+"\"").Start()
case "windows":
exec.Command("start", "cmd", "/k", command).Start()
default:
panic("Opening a terminal is not supported on " + runtime.GOOS)
}
}
type DockerGUITab struct {
*qt.QWidget
tree *qt.QTreeWidget
search_bar *qt.QLineEdit
}
type DockerGUI struct {
*qt.QMainWindow
toolbar *qt.QToolBar
start_action *qt.QAction
stop_action *qt.QAction
remove_action *qt.QAction
create_network_action *qt.QAction
remove_network_action *qt.QAction
create_volume_action *qt.QAction
remove_volume_action *qt.QAction
terminal_action *qt.QAction
pull_image_action *qt.QAction
remove_image_action *qt.QAction
logs_action *qt.QAction
auto_refresh_checkbox *qt.QCheckBox
tab_widget *qt.QTabWidget
containers_tab *qt.QWidget
images_tab *qt.QWidget
networks_tab *qt.QWidget
volumes_tab *qt.QWidget
containers_tree *qt.QTreeWidget
images_tree *qt.QTreeWidget
networks_tree *qt.QTreeWidget
volumes_tree *qt.QTreeWidget
containers_searchbar *qt.QLineEdit
images_searchbar *qt.QLineEdit
networks_searchbar *qt.QLineEdit
volumes_searchbar *qt.QLineEdit
refresh_timer *time.Ticker
}
func NewDockerGUI() *DockerGUI {
var self DockerGUI
self.QMainWindow = qt.NewQMainWindow2()
self.SetWindowTitle("Qocker - Docker Graphical User Interface")
self.SetGeometry(100, 100, 1000, 600)
// Create central widget and layout
central_widget := qt.NewQWidget2()
self.SetCentralWidget(central_widget)
mainLayout := qt.NewQVBoxLayout(central_widget)
// Create toolbar
self.create_toolbar()
// Create tab widget
self.tab_widget = qt.NewQTabWidget2()
mainLayout.AddWidget(self.tab_widget.QWidget)
// Create tabs
self.containers_tab = qt.NewQWidget2()
self.images_tab = qt.NewQWidget2()
self.networks_tab = qt.NewQWidget2()
self.volumes_tab = qt.NewQWidget2()
self.tab_widget.AddTab(self.containers_tab, "Containers")
self.tab_widget.AddTab(self.images_tab, "Images")
self.tab_widget.AddTab(self.networks_tab, "Networks")
self.tab_widget.AddTab(self.volumes_tab, "Volumes")
// Connect tab change to toolbar update
self.tab_widget.OnCurrentChanged(self.update_toolbar_buttons)
// Create tree widgets for each tab
self.containers_tree = self.create_tree_widget("ID", "Name", "Image", "Status", "Ports")
self.images_tree = self.create_tree_widget("ID", "Repository", "Tag", "Size")
self.networks_tree = self.create_tree_widget("ID", "Name", "Driver")
self.volumes_tree = self.create_tree_widget("Name", "Driver", "Mountpoint")
self.containers_tree.OnItemDoubleClicked(func(item *qt.QTreeWidgetItem, column int) {
self.open_terminal()
})
// Add search bars
self.containers_searchbar = self.create_searchbar_widget(self.containers_tree, "Search containers...")
self.images_searchbar = self.create_searchbar_widget(self.images_tree, "Search images...")
self.networks_searchbar = self.create_searchbar_widget(self.networks_tree, "Search networks...")
self.volumes_searchbar = self.create_searchbar_widget(self.volumes_tree, "Search volumes...")
// Add tree widgets to tabs
self.setup_tab(self.containers_tab, self.containers_tree, self.containers_searchbar)
self.setup_tab(self.images_tab, self.images_tree, self.images_searchbar)
self.setup_tab(self.networks_tab, self.networks_tree, self.networks_searchbar)
self.setup_tab(self.volumes_tab, self.volumes_tree, self.volumes_searchbar)
// Create menu bar
self.create_menu_bar()
//.Setup auto-refresh
self.setup_auto_refresh()
// Populate data
self.refresh_data()
// Update toolbar buttons for initial state
self.update_toolbar_buttons(0)
// override QMainWindow.createPopupMenu
self.OnCreatePopupMenu(func(super func() *qt.QMenu) *qt.QMenu {
filtered_menu := super()
filtered_menu.RemoveAction(self.toolbar.ToggleViewAction())
return filtered_menu
})
return &self
}
func (self *DockerGUI) create_tree_widget(headers ...string) *qt.QTreeWidget {
tree := qt.NewQTreeWidget2()
tree.SetHeaderLabels(headers)
tree.SetContextMenuPolicy(qt.CustomContextMenu)
tree.OnCustomContextMenuRequested(self.show_context_menu)
tree.SetIndentation(0)
tree.SetUniformRowHeights(true) // Speedup
tree.SetSortingEnabled(true)
return tree
}
// Add search bar
func (self *DockerGUI) create_searchbar_widget(tree *qt.QTreeWidget, search_placeholder string) *qt.QLineEdit {
search_bar := qt.NewQLineEdit2()
search_bar.SetPlaceholderText(search_placeholder)
search_bar.OnTextChanged(func(text string) {
self.filter_tree(tree, text)
})
return search_bar
}
func (self *DockerGUI) setup_tab(tab *qt.QWidget, tree *qt.QTreeWidget, search_bar *qt.QLineEdit) {
layout := qt.NewQVBoxLayout(tab)
layout.AddWidget(search_bar.QWidget)
layout.AddWidget(tree.QWidget)
}
func (self *DockerGUI) filter_tree(tree *qt.QTreeWidget, text string) {
text = strings.ToLower(text)
for i := 0; i < tree.TopLevelItemCount(); i++ {
item := tree.TopLevelItem(i)
match := false
for j := 0; j < item.ColumnCount(); j++ {
if strings.Contains(strings.ToLower(item.Text(j)), text) {
match = true
break
}
}
item.SetHidden(!match)
}
}
func (self *DockerGUI) create_toolbar() {
self.toolbar = qt.NewQToolBar3()
self.toolbar.SetMovable(false) // Make toolbar fixed
self.AddToolBar(qt.TopToolBarArea, self.toolbar)
// Common actions
refresh_action := qt.NewQAction6(qt.QIcon_FromTheme("view-refresh"), "Refresh", self.QObject)
refresh_action.OnTriggered(self.refresh_data)
self.toolbar.AddAction(refresh_action)
// Add auto-refresh checkbox
self.auto_refresh_checkbox = qt.NewQCheckBox3("Auto-refresh")
self.auto_refresh_checkbox.SetChecked(true)
self.toolbar.AddWidget(self.auto_refresh_checkbox.QWidget)
// Add separator
self.toolbar.AddSeparator()
// Container-specific actions
self.start_action = qt.NewQAction6(qt.QIcon_FromTheme("media-playback-start"), "Start", self.QObject)
self.start_action.OnTriggered(self.start_container)
self.toolbar.AddAction(self.start_action)
self.stop_action = qt.NewQAction6(qt.QIcon_FromTheme("media-playback-stop"), "Stop", self.QObject)
self.stop_action.OnTriggered(self.stop_container)
self.toolbar.AddAction(self.stop_action)
self.remove_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove", self.QObject)
self.remove_action.OnTriggered(self.remove_container)
self.toolbar.AddAction(self.remove_action)
// Image-specific actions
self.pull_image_action = qt.NewQAction6(qt.QIcon_FromTheme("download"), "Pull Image", self.QObject)
self.pull_image_action.OnTriggered(self.pull_image)
self.toolbar.AddAction(self.pull_image_action)
self.remove_image_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Image", self.QObject)
self.remove_image_action.OnTriggered(self.remove_image)
self.toolbar.AddAction(self.remove_image_action)
// Network-specific actions
self.create_network_action = qt.NewQAction6(qt.QIcon_FromTheme("list-add"), "Create Network", self.QObject)
self.create_network_action.OnTriggered(self.create_network)
self.toolbar.AddAction(self.create_network_action)
self.remove_network_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Network", self.QObject)
self.remove_network_action.OnTriggered(self.remove_network)
self.toolbar.AddAction(self.remove_network_action)
// Volume-specific actions
self.create_volume_action = qt.NewQAction6(qt.QIcon_FromTheme("list-add"), "Create Volume", self.QObject)
self.create_volume_action.OnTriggered(self.create_volume)
self.toolbar.AddAction(self.create_volume_action)
self.remove_volume_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Volume", self.QObject)
self.remove_volume_action.OnTriggered(self.remove_volume)
self.toolbar.AddAction(self.remove_volume_action)
// Add terminal action
self.terminal_action = qt.NewQAction6(qt.QIcon_FromTheme("utilities-terminal"), "Open Terminal", self.QObject)
self.terminal_action.OnTriggered(self.open_terminal)
self.toolbar.AddAction(self.terminal_action)
// Add logs action
self.logs_action = qt.NewQAction6(qt.QIcon_FromTheme("document-open"), "Open Logs", self.QObject)
self.logs_action.OnTriggered(self.open_logs)
self.toolbar.AddAction(self.logs_action)
}
func (self *DockerGUI) update_toolbar_buttons(index int) {
// Show actions based on the current tab
// Containers tab
self.start_action.SetVisible(index == ContainersTab)
self.stop_action.SetVisible(index == ContainersTab)
self.remove_action.SetVisible(index == ContainersTab)
self.terminal_action.SetVisible(index == ContainersTab)
self.logs_action.SetVisible(index == ContainersTab)
// Images tab
self.pull_image_action.SetVisible(index == ImagesTab)
self.remove_image_action.SetVisible(index == ImagesTab)
// Networks tab
self.create_network_action.SetVisible(index == NetworksTab)
self.remove_network_action.SetVisible(index == NetworksTab)
// Volumes tab
self.create_volume_action.SetVisible(index == VolumesTab)
self.remove_volume_action.SetVisible(index == VolumesTab)
}
func (self *DockerGUI) create_menu_bar() {
menubar := self.MenuBar()
file_menu := menubar.AddMenuWithTitle("&File")
exit_action := qt.NewQAction5("E&xit", self.QObject)
exit_action.OnTriggered(func() {
self.Close()
})
file_menu.AddAction(exit_action)
docker_menu := menubar.AddMenuWithTitle("&Docker")
refresh_action := qt.NewQAction5("&Refresh", self.QObject)
refresh_action.OnTriggered(self.refresh_data)
docker_menu.AddAction(refresh_action)
}
func (self *DockerGUI) show_context_menu(position *qt.QPoint) {
context_menu := qt.NewQMenu2()
current_tab := self.tab_widget.CurrentIndex()
// Add refresh action to context menu
refresh_action := qt.NewQAction5("&Refresh", self.QObject)
refresh_action.OnTriggered(self.refresh_data)
context_menu.AddAction(refresh_action)
context_menu.AddSeparator()
if current_tab == ContainersTab {
terminal_action := qt.NewQAction5("&Terminal", self.QObject)
terminal_action.OnTriggered(func() { self.handle_action("Terminal") })
context_menu.AddAction(terminal_action)
logs_action := qt.NewQAction5("&Logs", self.QObject)
logs_action.OnTriggered(func() { self.open_logs() })
context_menu.AddAction(logs_action)
context_menu.AddSeparator()
start_action := qt.NewQAction5("&Start", self.QObject)
start_action.OnTriggered(func() { self.handle_action("Start") })
context_menu.AddAction(start_action)
stop_action := qt.NewQAction5("S&top", self.QObject)
stop_action.OnTriggered(func() { self.handle_action("Stop") })
context_menu.AddAction(stop_action)
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
remove_action.OnTriggered(func() { self.handle_action("Remove") })
context_menu.AddAction(remove_action)
context_menu.ExecWithPos(self.containers_tree.Viewport().MapToGlobalWithQPoint(position))
} else if current_tab == ImagesTab {
pull_action := qt.NewQAction5("&Pull", self.QObject)
pull_action.OnTriggered(self.pull_image)
context_menu.AddAction(pull_action)
context_menu.AddSeparator()
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
remove_action.OnTriggered(self.remove_image)
context_menu.AddAction(remove_action)
context_menu.ExecWithPos(self.images_tree.Viewport().MapToGlobalWithQPoint(position))
} else if current_tab == NetworksTab {
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
remove_action.OnTriggered(func() { self.handle_action("Remove") })
context_menu.AddAction(remove_action)
context_menu.ExecWithPos(self.networks_tree.Viewport().MapToGlobalWithQPoint(position))
} else if current_tab == VolumesTab {
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
remove_action.OnTriggered(func() { self.handle_action("Remove") })
context_menu.AddAction(remove_action)
context_menu.ExecWithPos(self.volumes_tree.Viewport().MapToGlobalWithQPoint(position))
}
}
func (self *DockerGUI) setup_auto_refresh() {
self.refresh_timer = time.NewTicker(AutoRefreshInterval)
go func() {
for {
_, ok := <-self.refresh_timer.C
if !ok {
return
}
mainthread.Wait(func() {
if !self.auto_refresh_checkbox.IsChecked() {
return
}
self.refresh_data()
})
}
}()
}
func (self *DockerGUI) handle_action(action string) {
current_tab := self.tab_widget.CurrentIndex()
if current_tab == ContainersTab {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
return
}
container_id := selected_items[0].Text(0)
if action == "Terminal" {
self.open_terminal()
} else if action == "Start" {
dockerCommand("start", container_id).Run()
} else if action == "Stop" {
dockerCommand("stop", container_id).Run()
} else if action == "Remove" {
dockerCommand("rm", "-f", container_id).Run()
}
} else if current_tab == NetworksTab {
selected_items := self.networks_tree.SelectedItems()
if len(selected_items) == 0 {
return
}
network_id := selected_items[0].Text(0)
if action == "Remove" {
dockerCommand("network", "rm", network_id).Run()
}
} else if current_tab == VolumesTab {
selected_items := self.volumes_tree.SelectedItems()
if len(selected_items) == 0 {
return
}
volume_name := selected_items[0].Text(0)
if action == "Remove" {
dockerCommand("volume", "rm", volume_name).Run()
}
}
self.refresh_data()
}
func (self *DockerGUI) refresh_data() {
self.refresh_containers()
self.refresh_images()
self.refresh_networks()
self.refresh_volumes()
}
func (self *DockerGUI) refresh_containers() {
output, err := dockerCommand("ps", "-a", "--format", "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}").Output()
if err != nil {
panic(err)
}
scroll_position := self.containers_tree.VerticalScrollBar().Value()
selected_items := self.get_selected_items(self.containers_tree)
self.containers_tree.Clear()
containers := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, container := range containers {
parts := strings.Split(container, "\t")
if len(parts) == 1 {
continue
}
id := parts[0]
name := parts[1]
image := parts[2]
status := parts[3]
ports := ""
if len(parts) > 4 {
ports = parts[4]
}
item := qt.NewQTreeWidgetItem2([]string{id, name, image, "", ports}) // Empty string for status column
status_widget := NewStatusDelegate(status)
self.containers_tree.AddTopLevelItem(item)
self.containers_tree.SetItemWidget(item, 3, status_widget)
}
self.filter_tree(self.containers_tree, self.containers_searchbar.Text())
self.restore_selection(self.containers_tree, selected_items)
self.containers_tree.VerticalScrollBar().SetValue(scroll_position)
}
func (self *DockerGUI) refresh_images() {
output, err := dockerCommand("images", "--format", "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.Size}}").Output()
if err != nil {
panic(err)
}
scroll_position := self.images_tree.VerticalScrollBar().Value()
selected_items := self.get_selected_items(self.images_tree)
self.images_tree.Clear()
images := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, image := range images {
parts := strings.Split(image, "\t")
if len(parts) == 1 {
continue
}
id := parts[0]
repository := parts[1]
tag := parts[2]
size := parts[3]
item := qt.NewQTreeWidgetItem2([]string{id, repository, tag, size})
self.images_tree.AddTopLevelItem(item)
}
self.filter_tree(self.images_tree, self.images_searchbar.Text())
self.restore_selection(self.images_tree, selected_items)
self.images_tree.VerticalScrollBar().SetValue(scroll_position)
}
func (self *DockerGUI) refresh_networks() {
output, err := dockerCommand("network", "ls", "--format", "{{.ID}}\\t{{.Name}}\\t{{.Driver}}").Output()
if err != nil {
panic(err)
}
scroll_position := self.networks_tree.VerticalScrollBar().Value()
selected_items := self.get_selected_items(self.networks_tree)
self.networks_tree.Clear()
networks := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, network := range networks {
parts := strings.Split(network, "\t")
if len(parts) == 1 {
continue
}
id := parts[0]
name := parts[1]
driver := parts[2]
item := qt.NewQTreeWidgetItem2([]string{id, name, driver})
self.networks_tree.AddTopLevelItem(item)
}
self.filter_tree(self.networks_tree, self.networks_searchbar.Text())
self.restore_selection(self.networks_tree, selected_items)
self.networks_tree.VerticalScrollBar().SetValue(scroll_position)
}
func (self *DockerGUI) refresh_volumes() {
output, err := dockerCommand("volume", "ls", "--format", "{{.Name}}\\t{{.Driver}}\\t{{.Mountpoint}}").Output()
if err != nil {
panic(err)
}
scroll_position := self.volumes_tree.VerticalScrollBar().Value()
selected_items := self.get_selected_items(self.volumes_tree)
self.volumes_tree.Clear()
volumes := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, volume := range volumes {
parts := strings.Split(volume, "\t")
if len(parts) == 1 {
continue
}
name := parts[0]
driver := parts[1]
mountpoint := parts[2]
item := qt.NewQTreeWidgetItem2([]string{name, driver, mountpoint})
self.volumes_tree.AddTopLevelItem(item)
}
self.filter_tree(self.volumes_tree, self.volumes_searchbar.Text())
self.restore_selection(self.volumes_tree, selected_items)
self.volumes_tree.VerticalScrollBar().SetValue(scroll_position)
}
func (self *DockerGUI) get_selected_items(tree *qt.QTreeWidget) []string {
selItems := tree.SelectedItems()
ret := make([]string, 0, len(selItems))
for _, itm := range selItems {
ret = append(ret, itm.Text(0))
}
return ret
}
func (self *DockerGUI) restore_selection(tree *qt.QTreeWidget, selected_items []string) {
rmatch := map[string]struct{}{}
for _, itm := range selected_items {
rmatch[itm] = struct{}{}
}
for i := 0; i < tree.TopLevelItemCount(); i++ {
item := tree.TopLevelItem(i)
if _, ok := rmatch[item.Text(0)]; ok {
item.SetSelected(true)
}
}
}
func (self *DockerGUI) start_container() {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to start.")
return
}
for _, item := range selected_items {
container_id := item.Text(0)
err := dockerCommand("start", container_id).Wait()
if err != nil {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to start container "+container_id+": "+err.Error())
}
}
self.refresh_containers()
}
func (self *DockerGUI) stop_container() {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to stop.")
return
}
for _, item := range selected_items {
container_id := item.Text(0)
err := dockerCommand("stop", container_id).Run()
if err != nil {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to stop container "+container_id+": "+err.Error())
}
}
self.refresh_containers()
}
func (self *DockerGUI) remove_container() {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to remove.")
return
}
for _, item := range selected_items {
container_id := item.Text(0)
reply := qt.QMessageBox_Question5(self.QWidget,
"Confirm Removal",
"Are you sure you want to remove container "+container_id+"?",
qt.QMessageBox__Yes|qt.QMessageBox__No,
qt.QMessageBox__No,
)
if reply != qt.QMessageBox__Yes {
continue
}
err := dockerCommand("rm", "-f", container_id).Run()
if err != nil {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove container "+container_id+": "+err.Error())
}
}
self.refresh_containers()
}
func (self *DockerGUI) pull_image() {
image_name := qt.QInputDialog_GetText(self.QWidget, "Pull Image", "Enter image name (e.g., ubuntu:latest):")
if image_name == "" {
return
}
err := dockerCommand("pull", image_name).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Image \""+image_name+"\" pulled successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to pull image: "+err.Error())
}
self.refresh_images()
}
func (self *DockerGUI) remove_image() {
selected_items := self.images_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select an image to remove.")
return
}
for _, item := range selected_items {
image_id := item.Text(0)
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
"Are you sure you want to remove image "+image_id+"?",
qt.QMessageBox__Yes|qt.QMessageBox__No,
qt.QMessageBox__No,
)
if reply != qt.QMessageBox__Yes {
continue
}
err := dockerCommand("rmi", image_id).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Image \""+image_id+"\" removed successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove image "+image_id+": "+err.Error())
}
}
self.refresh_images()
}
func (self *DockerGUI) create_network() {
network_name := qt.QInputDialog_GetText(self.QWidget, "Create Network", "Enter network name:")
if network_name == "" {
return
}
err := dockerCommand("network", "create", network_name).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Network \""+network_name+"\" created successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to create network: "+err.Error())
}
self.refresh_networks()
}
func (self *DockerGUI) remove_network() {
selected_items := self.networks_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a network to remove.")
return
}
for _, item := range selected_items {
network_name := item.Text(1) // Assuming the network name is in the second column
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
"Are you sure you want to remove network "+network_name+"?",
qt.QMessageBox__Yes|qt.QMessageBox__No,
qt.QMessageBox__No,
)
if reply != qt.QMessageBox__Yes {
continue
}
err := dockerCommand("network", "rm", network_name).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Network \""+network_name+"\" removed successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove network "+network_name+": "+err.Error())
}
}
self.refresh_networks()
}
func (self *DockerGUI) create_volume() {
volume_name := qt.QInputDialog_GetText(self.QWidget, "Create Volume", "Enter volume name:")
if volume_name == "" {
return
}
err := dockerCommand("volume", "create", volume_name).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Volume \""+volume_name+"\" created successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to create volume "+volume_name+": "+err.Error())
}
self.refresh_volumes()
}
func (self *DockerGUI) remove_volume() {
selected_items := self.volumes_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a volume to remove.")
return
}
for _, item := range selected_items {
volume_name := item.Text(0) // Assuming the volume name is in the first column
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
"Are you sure you want to remove volume "+volume_name+"?",
qt.QMessageBox__Yes|qt.QMessageBox__No,
qt.QMessageBox__No,
)
if reply != qt.QMessageBox__Yes {
continue
}
err := dockerCommand("volume", "rm", volume_name).Run()
if err == nil {
qt.QMessageBox_Information(self.QWidget, "Success", "Volume \""+volume_name+"\" removed successfully.")
} else {
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove volume "+volume_name+": "+err.Error())
}
}
self.refresh_volumes()
}
func (self *DockerGUI) open_terminal() {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to open terminal.")
return
}
container_id := selected_items[0].Text(0)
openTerminal(container_id)
}
func (self *DockerGUI) show_terminal_error(error_message string) {
qt.QMessageBox_Critical(self.QWidget, "Error", error_message)
}
func (self *DockerGUI) open_logs() {
selected_items := self.containers_tree.SelectedItems()
if len(selected_items) == 0 {
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to open logs.")
return
}
container_id := selected_items[0].Text(0)
openLogs(container_id)
}
func (self *DockerGUI) show_logs_error(error_message string) {
qt.QMessageBox_Critical(self.QWidget, "Error", error_message)
}
func main() {
qt.NewQApplication(os.Args)
for _, arg := range os.Args {
if arg == `--sudo` {
dockerSudo = true
}
}
window := NewDockerGUI()
window.Show()
os.Exit(qt.QApplication_Exec())
}

664
main.py
View File

@ -1,664 +0,0 @@
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTabWidget, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QHBoxLayout, QWidget, QToolBar, QAction, QMenu,
QHeaderView, QLabel, QLineEdit, QCheckBox, QMessageBox, QInputDialog)
from PyQt5.QtGui import QIcon, QColor
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
import subprocess
import platform
class StatusDelegate(QWidget):
def __init__(self, status, parent=None):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(8)
self.status_circle = QWidget()
self.status_circle.setFixedSize(12, 12)
color = QColor('green') if 'Up' in status else QColor('red')
self.status_circle.setStyleSheet(f"background-color: {color.name()}; border-radius: 6px;")
self.status_label = QLabel(status)
layout.addWidget(self.status_circle)
layout.addWidget(self.status_label)
layout.addStretch()
class TerminalOpener(QThread):
error = pyqtSignal(str)
def __init__(self, container_id):
super().__init__()
self.container_id = container_id
def run(self):
docker_command = f"docker exec -it {self.container_id} sh -c '[ -x /bin/bash ] && exec /bin/bash || exec /bin/sh'"
system = platform.system()
try:
if system == "Darwin": # macOS
subprocess.Popen(['open', '-a', 'Terminal', '--', 'sh', '-c', f"{docker_command}"])
elif system == "Linux":
subprocess.Popen(['x-terminal-emulator', '-e', f'sh -c "{docker_command}"'])
elif system == "Windows":
subprocess.Popen(['start', 'cmd', '/k', docker_command], shell=True)
else:
self.error.emit(f"Opening a terminal is not supported on {system}")
except Exception as e:
self.error.emit(f"Failed to open terminal: {str(e)}")
class LogsOpener(QThread):
error = pyqtSignal(str)
def __init__(self, container_id):
super().__init__()
self.container_id = container_id
def run(self):
docker_command = f"docker logs -f {self.container_id}"
system = platform.system()
try:
if system == "Darwin": # macOS
subprocess.Popen(['open', '-a', 'Terminal', '--', 'sh', '-c', f"{docker_command}"])
elif system == "Linux":
subprocess.Popen(['x-terminal-emulator', '-e', f'sh -c "{docker_command}"'])
elif system == "Windows":
subprocess.Popen(['start', 'cmd', '/k', docker_command], shell=True)
else:
self.error.emit(f"Opening a terminal is not supported on {system}")
except Exception as e:
self.error.emit(f"Failed to open terminal: {str(e)}")
class DockerGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Qocker - Docker Graphical User Interface")
self.setGeometry(100, 100, 1000, 600)
# Create central widget and layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# Create toolbar
self.create_toolbar()
# Create tab widget
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget)
# Create tabs
self.containers_tab = QWidget()
self.images_tab = QWidget()
self.networks_tab = QWidget()
self.volumes_tab = QWidget()
self.tab_widget.addTab(self.containers_tab, "Containers")
self.tab_widget.addTab(self.images_tab, "Images")
self.tab_widget.addTab(self.networks_tab, "Networks")
self.tab_widget.addTab(self.volumes_tab, "Volumes")
# Connect tab change to toolbar update
self.tab_widget.currentChanged.connect(self.update_toolbar_buttons)
# Create tree widgets for each tab
self.containers_tree = self.create_tree_widget(["ID", "Name", "Image", "Status", "Ports"])
self.images_tree = self.create_tree_widget(["ID", "Repository", "Tag", "Size"])
self.networks_tree = self.create_tree_widget(["ID", "Name", "Driver"])
self.volumes_tree = self.create_tree_widget(["Name", "Driver", "Mountpoint"])
self.containers_tree.itemDoubleClicked.connect(self.open_terminal)
# Add tree widgets to tabs
self.setup_tab(self.containers_tab, self.containers_tree, "Search containers...")
self.setup_tab(self.images_tab, self.images_tree, "Search images...")
self.setup_tab(self.networks_tab, self.networks_tree, "Search networks...")
self.setup_tab(self.volumes_tab, self.volumes_tree, "Search volumes...")
# Create menu bar
self.create_menu_bar()
# Setup auto-refresh
self.setup_auto_refresh()
# Populate data
self.refresh_data()
# Update toolbar buttons for initial state
self.update_toolbar_buttons(0)
def create_tree_widget(self, headers):
tree = QTreeWidget()
tree.setHeaderLabels(headers)
tree.setContextMenuPolicy(Qt.CustomContextMenu)
tree.customContextMenuRequested.connect(self.show_context_menu)
tree.header().setSectionResizeMode(QHeaderView.Interactive)
tree.header().setSortIndicator(0, Qt.DescendingOrder)
tree.header().setSortIndicatorShown(True)
tree.header().sortIndicatorChanged.connect(lambda col, order: self.sort_tree_widget(tree, col, order))
return tree
def sort_tree_widget(self, tree, column, order):
tree.sortItems(column, order)
def setup_tab(self, tab, tree, search_placeholder):
layout = QVBoxLayout(tab)
# Add search bar
search_bar = QLineEdit()
search_bar.setPlaceholderText(search_placeholder)
search_bar.textChanged.connect(lambda text: self.filter_tree(tree, text))
layout.addWidget(search_bar)
layout.addWidget(tree)
def filter_tree(self, tree, text):
for i in range(tree.topLevelItemCount()):
item = tree.topLevelItem(i)
match = any(text.lower() in item.text(j).lower() for j in range(item.columnCount()))
item.setHidden(not match)
def create_toolbar(self):
self.toolbar = QToolBar()
self.toolbar.setMovable(False) # Make toolbar fixed
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
# Common actions
self.refresh_action = QAction(QIcon.fromTheme("view-refresh"), "Refresh", self)
self.refresh_action.triggered.connect(self.refresh_data)
self.toolbar.addAction(self.refresh_action)
# Add auto-refresh checkbox
self.auto_refresh_checkbox = QCheckBox("Auto-refresh")
self.auto_refresh_checkbox.setChecked(True)
self.auto_refresh_checkbox.stateChanged.connect(self.toggle_auto_refresh)
self.toolbar.addWidget(self.auto_refresh_checkbox)
# Add separator
self.toolbar.addSeparator()
# Container-specific actions
self.start_action = QAction(QIcon.fromTheme("media-playback-start"), "Start", self)
self.start_action.triggered.connect(self.start_container)
self.toolbar.addAction(self.start_action)
self.stop_action = QAction(QIcon.fromTheme("media-playback-stop"), "Stop", self)
self.stop_action.triggered.connect(self.stop_container)
self.toolbar.addAction(self.stop_action)
self.remove_action = QAction(QIcon.fromTheme("edit-delete"), "Remove", self)
self.remove_action.triggered.connect(self.remove_container)
self.toolbar.addAction(self.remove_action)
# Image-specific actions
self.pull_image_action = QAction(QIcon.fromTheme("download"), "Pull Image", self)
self.pull_image_action.triggered.connect(self.pull_image)
self.toolbar.addAction(self.pull_image_action)
self.remove_image_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Image", self)
self.remove_image_action.triggered.connect(self.remove_image)
self.toolbar.addAction(self.remove_image_action)
# Network-specific actions
self.create_network_action = QAction(QIcon.fromTheme("list-add"), "Create Network", self)
self.create_network_action.triggered.connect(self.create_network)
self.toolbar.addAction(self.create_network_action)
self.remove_network_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Network", self)
self.remove_network_action.triggered.connect(self.remove_network)
self.toolbar.addAction(self.remove_network_action)
# Volume-specific actions
self.create_volume_action = QAction(QIcon.fromTheme("list-add"), "Create Volume", self)
self.create_volume_action.triggered.connect(self.create_volume)
self.toolbar.addAction(self.create_volume_action)
self.remove_volume_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Volume", self)
self.remove_volume_action.triggered.connect(self.remove_volume)
self.toolbar.addAction(self.remove_volume_action)
# Add terminal action
self.terminal_action = QAction(QIcon.fromTheme("utilities-terminal"), "Open Terminal", self)
self.terminal_action.triggered.connect(self.open_terminal)
self.toolbar.addAction(self.terminal_action)
# Add logs action
self.logs_action = QAction(QIcon.fromTheme("document-open"), "Open Logs", self)
self.logs_action.triggered.connect(self.open_logs)
self.toolbar.addAction(self.logs_action)
def update_toolbar_buttons(self, index):
# Hide all specific actions
self.start_action.setVisible(False)
self.stop_action.setVisible(False)
self.remove_action.setVisible(False)
self.create_network_action.setVisible(False)
self.remove_network_action.setVisible(False)
self.create_volume_action.setVisible(False)
self.remove_volume_action.setVisible(False)
self.terminal_action.setVisible(False)
self.pull_image_action.setVisible(False)
self.remove_image_action.setVisible(False)
self.logs_action.setVisible(False)
# Show actions based on the current tab
if index == 0: # Containers tab
self.start_action.setVisible(True)
self.stop_action.setVisible(True)
self.remove_action.setVisible(True)
self.terminal_action.setVisible(True)
self.logs_action.setVisible(True)
elif index == 1: # Images tab
self.pull_image_action.setVisible(True)
self.remove_image_action.setVisible(True)
elif index == 2: # Networks tab
self.create_network_action.setVisible(True)
self.remove_network_action.setVisible(True)
elif index == 3: # Volumes tab
self.create_volume_action.setVisible(True)
self.remove_volume_action.setVisible(True)
# override QMainWindow.createPopupMenu
def createPopupMenu(self):
filtered_menu = super(DockerGUI, self).createPopupMenu()
filtered_menu.removeAction(self.toolbar.toggleViewAction())
return filtered_menu
def update_visible_tabs(self, index):
for i in range(self.tab_widget.count()):
if i == index:
self.tab_widget.tabBar().setTabVisible(i, True)
else:
self.tab_widget.tabBar().setTabVisible(i, False)
def create_menu_bar(self):
menubar = self.menuBar()
file_menu = menubar.addMenu("File")
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
docker_menu = menubar.addMenu("Docker")
refresh_action = QAction("Refresh", self)
refresh_action.triggered.connect(self.refresh_data)
docker_menu.addAction(refresh_action)
def show_context_menu(self, position):
context_menu = QMenu()
current_tab = self.tab_widget.currentWidget()
# Add refresh action to context menu
refresh_action = QAction("Refresh", self)
refresh_action.triggered.connect(self.refresh_data)
context_menu.addAction(refresh_action)
context_menu.addSeparator()
if current_tab == self.containers_tab:
terminal_action = QAction("Terminal", self)
terminal_action.triggered.connect(lambda: self.handle_action("Terminal"))
logs_action = QAction("Logs", self)
logs_action.triggered.connect(lambda: self.open_logs())
start_action = QAction("Start", self)
start_action.triggered.connect(lambda: self.handle_action("Start"))
stop_action = QAction("Stop", self)
stop_action.triggered.connect(lambda: self.handle_action("Stop"))
remove_action = QAction("Remove", self)
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
context_menu.addAction(terminal_action)
context_menu.addAction(logs_action)
context_menu.addSeparator()
context_menu.addAction(start_action)
context_menu.addAction(stop_action)
context_menu.addAction(remove_action)
elif current_tab == self.images_tab:
pull_action = QAction("Pull", self)
pull_action.triggered.connect(self.pull_image)
remove_action = QAction("Remove", self)
remove_action.triggered.connect(self.remove_image)
context_menu.addAction(pull_action)
context_menu.addSeparator()
context_menu.addAction(remove_action)
elif current_tab == self.networks_tab:
remove_action = QAction("Remove", self)
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
context_menu.addAction(remove_action)
elif current_tab == self.volumes_tab:
remove_action = QAction("Remove", self)
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
context_menu.addAction(remove_action)
context_menu.exec_(current_tab.mapToGlobal(position))
def setup_auto_refresh(self):
self.refresh_timer = QTimer(self)
self.refresh_timer.timeout.connect(self.refresh_data)
if self.auto_refresh_checkbox.isChecked():
self.refresh_timer.start(1000) # 1000 ms = 1 second
def toggle_auto_refresh(self, state):
if state == Qt.Checked:
self.refresh_timer.start(1000)
else:
self.refresh_timer.stop()
def handle_action(self, action):
current_tab = self.tab_widget.currentWidget()
selected_items = current_tab.findChild(QTreeWidget).selectedItems()
if not selected_items:
return
if current_tab == self.containers_tab:
container_id = selected_items[0].text(0)
if action == "Terminal":
self.open_terminal()
elif action == "Start":
subprocess.run(["docker", "start", container_id])
elif action == "Stop":
subprocess.run(["docker", "stop", container_id])
elif action == "Remove":
subprocess.run(["docker", "rm", "-f", container_id])
elif current_tab == self.networks_tab:
network_id = selected_items[0].text(0)
if action == "Remove":
subprocess.run(["docker", "network", "rm", network_id])
elif current_tab == self.volumes_tab:
volume_name = selected_items[0].text(0)
if action == "Remove":
subprocess.run(["docker", "volume", "rm", volume_name])
self.refresh_data()
def refresh_data(self):
self.refresh_containers()
self.refresh_images()
self.refresh_networks()
self.refresh_volumes()
def refresh_containers(self):
scroll_position = self.containers_tree.verticalScrollBar().value()
selected_items = self.get_selected_items(self.containers_tree)
self.containers_tree.clear()
try:
output = subprocess.check_output(["docker", "ps", "-a", "--format", "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}"], stderr=subprocess.STDOUT)
if output.strip():
containers = output.decode().strip().split("\n")
for container in containers:
parts = container.split("\t")
id, name, image, status = parts[:4]
ports = parts[4] if len(parts) > 4 else ""
item = QTreeWidgetItem([id, name, image, "", ports]) # Empty string for status column
status_widget = StatusDelegate(status)
self.containers_tree.addTopLevelItem(item)
self.containers_tree.setItemWidget(item, 3, status_widget)
self.filter_tree(self.containers_tree, self.containers_tab.findChild(QLineEdit).text())
self.restore_selection(self.containers_tree, selected_items)
except subprocess.CalledProcessError as e:
print(f"Error refreshing containers: {e.output.decode()}")
except ValueError:
print(f"Error parsing container list {repr(containers)}")
except Exception as e:
print(f"Unexpected error refreshing containers: {str(e)}")
QTimer.singleShot(0, lambda: self.containers_tree.verticalScrollBar().setValue(scroll_position))
def refresh_images(self):
scroll_position = self.images_tree.verticalScrollBar().value()
selected_items = self.get_selected_items(self.images_tree)
self.images_tree.clear()
try:
output = subprocess.check_output(["docker", "images", "--format", "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.Size}}"], stderr=subprocess.STDOUT)
if output.strip():
images = output.decode().strip().split("\n")
for image in images:
id, repository, tag, size = image.split("\t")
item = QTreeWidgetItem([id, repository, tag, size])
self.images_tree.addTopLevelItem(item)
self.sort_tree_widget(self.images_tree, 0, Qt.DescendingOrder)
self.filter_tree(self.images_tree, self.images_tab.findChild(QLineEdit).text())
self.restore_selection(self.images_tree, selected_items)
except subprocess.CalledProcessError as e:
print(f"Error refreshing images: {e.output.decode()}")
except Exception as e:
print(f"Unexpected error refreshing images: {str(e)}")
QTimer.singleShot(0, lambda: self.images_tree.verticalScrollBar().setValue(scroll_position))
def refresh_networks(self):
scroll_position = self.networks_tree.verticalScrollBar().value()
selected_items = self.get_selected_items(self.networks_tree)
self.networks_tree.clear()
try:
output = subprocess.check_output(["docker", "network", "ls", "--format", "{{.ID}}\\t{{.Name}}\\t{{.Driver}}"], stderr=subprocess.STDOUT)
if output.strip():
networks = output.decode().strip().split("\n")
for network in networks:
id, name, driver = network.split("\t")
item = QTreeWidgetItem([id, name, driver])
self.networks_tree.addTopLevelItem(item)
self.filter_tree(self.networks_tree, self.networks_tab.findChild(QLineEdit).text())
self.restore_selection(self.networks_tree, selected_items)
except subprocess.CalledProcessError as e:
print(f"Error refreshing networks: {e.output.decode()}")
except Exception as e:
print(f"Unexpected error refreshing networks: {str(e)}")
QTimer.singleShot(0, lambda: self.networks_tree.verticalScrollBar().setValue(scroll_position))
def refresh_volumes(self):
scroll_position = self.volumes_tree.verticalScrollBar().value()
selected_items = self.get_selected_items(self.volumes_tree)
self.volumes_tree.clear()
try:
output = subprocess.check_output(["docker", "volume", "ls", "--format", "{{.Name}}\\t{{.Driver}}\\t{{.Mountpoint}}"], stderr=subprocess.STDOUT)
if output.strip():
volumes = output.decode().strip().split("\n")
for volume in volumes:
name, driver, mountpoint = volume.split("\t")
item = QTreeWidgetItem([name, driver, mountpoint])
self.volumes_tree.addTopLevelItem(item)
self.filter_tree(self.volumes_tree, self.volumes_tab.findChild(QLineEdit).text())
self.restore_selection(self.volumes_tree, selected_items)
except subprocess.CalledProcessError as e:
print(f"Error refreshing volumes: {e.output.decode()}")
except Exception as e:
print(f"Unexpected error refreshing volumes: {str(e)}")
QTimer.singleShot(0, lambda: self.volumes_tree.verticalScrollBar().setValue(scroll_position))
def get_selected_items(self, tree):
return [item.text(0) for item in tree.selectedItems()]
def restore_selection(self, tree, selected_items):
for i in range(tree.topLevelItemCount()):
item = tree.topLevelItem(i)
if item.text(0) in selected_items:
item.setSelected(True)
def start_container(self):
selected_items = self.containers_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a container to start.")
return
for item in selected_items:
container_id = item.text(0)
try:
subprocess.run(["docker", "start", container_id], check=True)
print(f"Started container: {container_id}")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to start container {container_id}: {e}")
self.refresh_containers()
def stop_container(self):
selected_items = self.containers_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a container to stop.")
return
for item in selected_items:
container_id = item.text(0)
try:
subprocess.run(["docker", "stop", container_id], check=True)
print(f"Stopped container: {container_id}")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to stop container {container_id}: {e}")
self.refresh_containers()
def remove_container(self):
selected_items = self.containers_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a container to remove.")
return
for item in selected_items:
container_id = item.text(0)
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove container {container_id}?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
try:
subprocess.run(["docker", "rm", "-f", container_id], check=True)
print(f"Removed container: {container_id}")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to remove container {container_id}: {e}")
self.refresh_containers()
def pull_image(self):
image_name, ok = QInputDialog.getText(self, "Pull Image", "Enter image name (e.g., ubuntu:latest):")
if ok and image_name:
try:
subprocess.run(["docker", "pull", image_name], check=True)
print(f"Pulled image: {image_name}")
QMessageBox.information(self, "Success", f"Image '{image_name}' pulled successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to pull image: {e}")
self.refresh_images()
def remove_image(self):
selected_items = self.images_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select an image to remove.")
return
for item in selected_items:
image_id = item.text(0)
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove image {image_id}?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
try:
subprocess.run(["docker", "rmi", image_id], check=True)
print(f"Removed image: {image_id}")
QMessageBox.information(self, "Success", f"Image '{image_id}' removed successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to remove image {image_id}: {e}")
self.refresh_images()
def create_network(self):
network_name, ok = QInputDialog.getText(self, "Create Network", "Enter network name:")
if ok and network_name:
try:
subprocess.run(["docker", "network", "create", network_name], check=True)
print(f"Created network: {network_name}")
QMessageBox.information(self, "Success", f"Network '{network_name}' created successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to create network: {e}")
self.refresh_networks()
def remove_network(self):
selected_items = self.networks_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a network to remove.")
return
for item in selected_items:
network_name = item.text(1) # Assuming the network name is in the second column
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove network {network_name}?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
try:
subprocess.run(["docker", "network", "rm", network_name], check=True)
print(f"Removed network: {network_name}")
QMessageBox.information(self, "Success", f"Network '{network_name}' removed successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to remove network {network_name}: {e}")
self.refresh_networks()
def create_volume(self):
volume_name, ok = QInputDialog.getText(self, "Create Volume", "Enter volume name:")
if ok and volume_name:
try:
subprocess.run(["docker", "volume", "create", volume_name], check=True)
print(f"Created volume: {volume_name}")
QMessageBox.information(self, "Success", f"Volume '{volume_name}' created successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to create volume: {e}")
self.refresh_volumes()
def remove_volume(self):
selected_items = self.volumes_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a volume to remove.")
return
for item in selected_items:
volume_name = item.text(0) # Assuming the volume name is in the first column
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove volume {volume_name}?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
try:
subprocess.run(["docker", "volume", "rm", volume_name], check=True)
print(f"Removed volume: {volume_name}")
QMessageBox.information(self, "Success", f"Volume '{volume_name}' removed successfully.")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "Error", f"Failed to remove volume {volume_name}: {e}")
self.refresh_volumes()
def open_terminal(self):
selected_items = self.containers_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a container to open terminal.")
return
container_id = selected_items[0].text(0)
self.terminal_opener = TerminalOpener(container_id)
self.terminal_opener.error.connect(self.show_terminal_error)
self.terminal_opener.start()
def show_terminal_error(self, error_message):
QMessageBox.critical(self, "Error", error_message)
def open_logs(self):
selected_items = self.containers_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "No Selection", "Please select a container to open logs.")
return
container_id = selected_items[0].text(0)
self.logs_opener = LogsOpener(container_id)
self.logs_opener.error.connect(self.show_logs_error)
self.logs_opener.start()
def show_logs_error(self, error_message):
QMessageBox.critical(self, "Error", error_message)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = DockerGUI()
window.show()
sys.exit(app.exec_())

View File

@ -1 +0,0 @@
pyqt5==5.15.10