Compare commits

..

No commits in common. "master" and "v0.2" have entirely different histories.
master ... v0.2

11 changed files with 798 additions and 955 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
qocker-miqt
/.idea
/venv
.flatpak-builder

View File

@ -1,14 +0,0 @@
# 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,17 +1,63 @@
# Qocker-miqt
# Qocker
Qocker-miqt is a user-friendly GUI application for managing Docker containers.
![Qocker Screenshot](./assets/screenshot.png)
This is a fork of [Qocker](https://github.com/xlmnxp/Qocker) ported to the [MIQT](https://github.com/mappu/miqt) library for demonstration purposes.
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.
## Building
## Features
```bash
apt install qt6-base-dev build-essential golang-go
go build -ldflags '-s -w'
./qocker-miqt
- **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
```
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.
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).

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

72
flatpak/sa.sy.qocker.json Normal file
View File

@ -0,0 +1,72 @@
{
"app-id": "sa.sy.Qocker",
"runtime": "org.kde.Platform",
"runtime-version": "5.15",
"sdk": "org.kde.Sdk",
"command": "qocker",
"modules": [
{
"name": "PyQt5",
"cleanup": [
"/bin/sip",
"/include",
"/lib/python3.9/site-packages/*.pyi"
],
"config-opts": [
"--disable-static",
"--enable-x11"
],
"buildsystem": "simple",
"build-commands": [
"python configure.py --confirm-license --no-docstrings --assume-shared --no-sip-files --no-qml-plugin --no-tools --no-qsci-api -d ${FLATPAK_DEST}/lib/python3.9/site-packages --sip=${FLATPAK_DEST}/bin/sip --sip-incdir=${FLATPAK_DEST}/include --stubsdir=${FLATPAK_DEST}/lib/python3.9/site-packages --disable=QtSensors --disable=QtWebEngine --disable=QtQuick --disable=QtQml --disable=QtWebChannel --disable=QtWebEngineCore --disable=QWebEngineWidgets --disable=QtQuickWidgets --disable=QtSql --disable=QtXmlPatterns --disable=QtMultimedia --disable=QtMultimediaWidgets --disable=QtLocation --disable=QtDesigner --disable=QtOpenGL --disable=QtBluetooth --disable=QtWebKit --disable=QtWebKitWidgets --disable=QtNfc --disable=QtPositioning",
"make -j $FLATPAK_BUILDER_N_JOBS",
"make install"
],
"sources": [
{
"type": "archive",
"url": "https://pypi.io/packages/source/P/PyQt5/PyQt5-5.15.4.tar.gz",
"sha256": "2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"
}
],
"modules": [
{
"name": "sip",
"buildsystem": "simple",
"build-commands": [
"python configure.py --sip-module PyQt5.sip -b ${FLATPAK_DEST}/bin -d ${FLATPAK_DEST}/lib/python3.9/site-packages -e ${FLATPAK_DEST}/include -v ${FLATPAK_DEST}/share/sip --stubsdir=${FLATPAK_DEST}/lib/python3.9/site-packages",
"make",
"make install"
],
"sources": [
{
"type": "archive",
"url": "https://www.riverbankcomputing.com/static/Downloads/sip/4.19.25/sip-4.19.25.tar.gz",
"sha256": "b39d93e937647807bac23579edbff25fe46d16213f708370072574ab1f1b4211"
}
]
}
]
},
{
"name": "qocker",
"buildsystem": "simple",
"build-commands": [
"install -Dm755 main.py /app/bin/qocker"
],
"sources": [
{
"type": "git",
"url": "https://github.com/xlmnxp/qocker.git",
"tag": "v0.2"
}
]
}
],
"finish-args": [
"--share=network",
"--socket=x11",
"--socket=pulseaudio",
"--env=QT_QPA_PLATFORM=xcb"
]
}

5
go.mod
View File

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

4
go.sum
View File

@ -1,4 +0,0 @@
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
View File

@ -1,920 +0,0 @@
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())
}

665
main.py Normal file
View File

@ -0,0 +1,665 @@
#!/usr/bin/env python3
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_())

1
requirements.txt Normal file
View File

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