Skip to content

Commit 70f5fd3

Browse files
committed
Додавання вкладки "Порти" для керування мережевими з’єднаннями
Тепер у програмі з’явилася нова вкладка "Порти", яка дозволяє переглядати всі активні TCP та UDP з’єднання на вашому комп’ютері. Що змінилося: - **Список портів**: Програма автоматично збирає дані про порти (за допомогою команди `ss`), групує їх за номером та показує, яка саме програма їх використовує. - **Звільнення порту**: Ви можете закрити порт прямо з інтерфейсу програми: - Якщо порт використовується контейнером Docker, програма зупинить цей контейнер. - Якщо порт належить іншому процесу, ви можете завершити його роботу (функція "Kill" надсилає сигнал завершення, а "Force" — примусово закриває процес). - Додано захист від випадкових дій: для завершення роботи процесів тепер потрібне підтвердження.
1 parent d69e4dc commit 70f5fd3

12 files changed

Lines changed: 379 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **Ports** tab: listening TCP/UDP sockets on the host (`ss -tulpn`), sorted by port with IPv4/IPv6 rows merged, showing the owning process and PID
13+
- Free a port from the popup: `docker stop` for ports published by a running container, `kill -TERM` (or `-KILL` via **Force**) for any other process, both behind a confirmation row
14+
815
## [0.2.0] - 2026-07-25
916

1017
### Added

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ path = "src/main.rs"
1919
required-features = ["applet"]
2020

2121
[dependencies]
22-
tokio = { version = "1", features = ["process", "rt"] }
22+
tokio = { version = "1", features = ["process", "rt", "time"] }
2323
libcosmic = { git = "https://github.com/pop-os/libcosmic.git", optional = true, features = [
2424
"applet",
2525
"tokio",

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ COSMIC panel applet for **Pop!_OS**: manage Docker containers from the panel —
66

77
- **Pop!_OS 24.04+** (or another distro with COSMIC panel)
88
- **Docker** CLI (`docker` in `PATH`, daemon running, user in `docker` group or rootless Docker)
9+
- `ss` from **iproute2** and `kill` from **procps** — both preinstalled on Pop!_OS; required by the **Ports** tab
910
- Rust 1.93 or newer (edition 2024; required by `libcosmic`)
1011
- System libraries for building `libcosmic`:
1112

@@ -74,9 +75,10 @@ flatpak-builder --user --install --force-clean build-dir \
7475

7576
In the popup:
7677

77-
- Tabs: **Containers**, **Images**, **Volumes**, **Settings**
78+
- Tabs: **Containers**, **Images**, **Volumes**, **Ports**, **Settings**
7879
- **Containers**: grouped by Compose project, **hide stopped** filter (hide icon), collapse groups, icon actions (play / stop / reload / delete), published ports and live CPU / RAM usage (`docker stats`)
7980
- **Images** / **Volumes**: list with delete (`docker rmi` / `docker volume rm`)
81+
- **Ports**: every listening TCP/UDP socket on the host (`ss -tulpn`), sorted by port, IPv4/IPv6 rows merged. A port published by a running container is freed with `docker stop`; any other socket is freed by signalling its process — **Kill** sends `SIGTERM`, **Force** sends `SIGKILL`. Sockets owned by another user (root) show no PID and no action, because `ss` cannot reveal their process
8082
- **Clean** (broom icon, top right): prunes the active tab — stopped containers, unused images (`-a`), or unused volumes; asks for confirmation and reports reclaimed space
8183
- **Settings**: panel running-count font size (10–28 pt), live preview; saved to `$XDG_CONFIG_HOME/io.github.vaulttec_dev.CosmicContainers/settings.conf` (migrated automatically from `~/.config/linux-pop-extension/` when present)
8284
- Auto-refresh every 5 seconds (panel count + open popup)

‎src/app/mod.rs‎

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use cosmic::iced::window;
1212
use cosmic::iced::window::Id;
1313
use cosmic_containers::{
1414
ContainerGroup, ContainerStats, ContainerSummary, ImageSummary, POLL_INTERVAL_SECS, PopupTab,
15-
UserSettings, VolumeSummary, group_containers, running_count, settings,
15+
PortEntry, UserSettings, VolumeSummary, group_containers, running_count, settings,
1616
};
1717

1818
pub use popup::POPUP_HEIGHT;
@@ -26,12 +26,20 @@ pub enum PendingRemove {
2626
Volume(String),
2727
}
2828

29+
/// Process awaiting kill confirmation in the ports tab.
30+
#[derive(Clone, Debug, PartialEq, Eq)]
31+
pub struct PendingKill {
32+
pub pid: u32,
33+
pub label: String,
34+
}
35+
2936
/// Per-tab zero-based page index for paginated list tabs.
3037
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3138
pub(super) struct TabPages {
3239
pub(super) containers: usize,
3340
pub(super) images: usize,
3441
pub(super) volumes: usize,
42+
pub(super) ports: usize,
3543
}
3644

3745
impl TabPages {
@@ -40,6 +48,7 @@ impl TabPages {
4048
PopupTab::Containers => Some(&mut self.containers),
4149
PopupTab::Images => Some(&mut self.images),
4250
PopupTab::Volumes => Some(&mut self.volumes),
51+
PopupTab::Ports => Some(&mut self.ports),
4352
PopupTab::Settings => None,
4453
}
4554
}
@@ -55,10 +64,12 @@ pub struct App {
5564
pub(super) groups: Vec<ContainerGroup>,
5665
pub(super) image_list: Vec<ImageSummary>,
5766
pub(super) volume_list: Vec<VolumeSummary>,
67+
pub(super) port_list: Vec<PortEntry>,
5868
pub(super) container_stats: HashMap<String, ContainerStats>,
5969
pub(super) collapsed_groups: HashSet<String>,
6070
pub(super) tab_pages: TabPages,
6171
pub(super) pending_remove: Option<PendingRemove>,
72+
pub(super) pending_kill: Option<PendingKill>,
6273
pub(super) pending_prune: bool,
6374
pub(super) notice: Option<String>,
6475
pub(super) loading: bool,
@@ -79,6 +90,10 @@ pub enum Message {
7990
RequestRemove(PendingRemove),
8091
ConfirmRemove,
8192
CancelRemove,
93+
RequestKill(PendingKill),
94+
ConfirmKill { force: bool },
95+
CancelKill,
96+
StopContainerPort(String),
8297
RequestPrune,
8398
ConfirmPrune,
8499
CancelPrune,
@@ -96,6 +111,10 @@ pub enum Message {
96111
result: Result<Vec<VolumeSummary>, String>,
97112
silent: bool,
98113
},
114+
PortsLoaded {
115+
result: Result<Vec<PortEntry>, String>,
116+
silent: bool,
117+
},
99118
Start(String),
100119
Stop(String),
101120
Restart(String),
@@ -124,14 +143,22 @@ impl App {
124143
})
125144
}
126145

146+
/// Rows a paginated tab currently holds (`None` for tabs without a list).
147+
pub(super) fn tab_item_count(&self, tab: PopupTab) -> Option<usize> {
148+
match tab {
149+
PopupTab::Containers => Some(self.container_row_count()),
150+
PopupTab::Images => Some(self.image_list.len()),
151+
PopupTab::Volumes => Some(self.volume_list.len()),
152+
PopupTab::Ports => Some(self.port_list.len()),
153+
PopupTab::Settings => None,
154+
}
155+
}
156+
127157
pub(super) fn clamp_tab_page(&mut self, tab: PopupTab) {
128158
use cosmic_containers::{LIST_PAGE_SIZE, clamp_page, total_pages};
129159

130-
let item_count = match tab {
131-
PopupTab::Containers => self.container_row_count(),
132-
PopupTab::Images => self.image_list.len(),
133-
PopupTab::Volumes => self.volume_list.len(),
134-
PopupTab::Settings => return,
160+
let Some(item_count) = self.tab_item_count(tab) else {
161+
return;
135162
};
136163
let total = total_pages(item_count, LIST_PAGE_SIZE);
137164
let Some(page) = self.tab_pages.page_mut(tab) else {
@@ -209,6 +236,35 @@ impl App {
209236
}
210237
}
211238

239+
pub(super) fn apply_ports(&mut self, result: Result<Vec<PortEntry>, String>, silent: bool) {
240+
if !silent {
241+
self.loading = false;
242+
}
243+
match result {
244+
Ok(port_list) => {
245+
self.port_list = port_list;
246+
self.clamp_tab_page(PopupTab::Ports);
247+
if self.active_tab == PopupTab::Ports {
248+
self.error = None;
249+
}
250+
}
251+
Err(err) => {
252+
if self.active_tab == PopupTab::Ports {
253+
self.error = Some(err);
254+
}
255+
}
256+
}
257+
}
258+
259+
/// Running container publishing `port`, if any (`docker-proxy` hides the real owner).
260+
pub(super) fn container_for_port(&self, port: u16) -> Option<&str> {
261+
self.containers
262+
.iter()
263+
.filter(|container| container.is_running())
264+
.find(|container| container.publishes_host_port(port))
265+
.map(|container| container.name.as_str())
266+
}
267+
212268
pub(super) fn apply_stats(&mut self, result: Result<Vec<ContainerStats>, String>) {
213269
// Resource usage is supplementary: a failing `docker stats` must not mask the list.
214270
self.container_stats = match result {
@@ -229,7 +285,7 @@ impl App {
229285
}
230286

231287
pub(super) fn fetch_tab_task(tab: PopupTab, silent: bool) -> Task<Message> {
232-
use cosmic_containers::services::{docker, images, volumes};
288+
use cosmic_containers::services::{docker, images, ports, volumes};
233289

234290
match tab {
235291
PopupTab::Containers => Task::perform(docker::list_containers(), move |result| {
@@ -241,6 +297,9 @@ impl App {
241297
PopupTab::Volumes => Task::perform(volumes::list_volumes(), move |result| {
242298
cosmic::Action::App(Message::VolumesLoaded { result, silent })
243299
}),
300+
PopupTab::Ports => Task::perform(ports::list_ports(), move |result| {
301+
cosmic::Action::App(Message::PortsLoaded { result, silent })
302+
}),
244303
PopupTab::Settings => Task::none(),
245304
}
246305
}

‎src/app/popup/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod containers;
22
mod images_tab;
33
mod layout;
4+
mod ports_tab;
45
mod settings_tab;
56
mod volumes_tab;
67
mod widgets;
@@ -53,6 +54,7 @@ pub(super) fn popup_view(state: &App) -> Element<'_, cosmic::Action<Message>> {
5354
PopupTab::Containers => containers::containers_tab(state),
5455
PopupTab::Images => images_tab::images_tab(state),
5556
PopupTab::Volumes => volumes_tab::volumes_tab(state),
57+
PopupTab::Ports => ports_tab::ports_tab(state),
5658
PopupTab::Settings => settings_tab::settings_tab(state),
5759
};
5860
content = content.push(
@@ -89,6 +91,7 @@ fn header_bar<'a>(state: &'a App) -> Element<'a, Message> {
8991
tab_button("Containers", PopupTab::Containers, state.active_tab),
9092
tab_button("Images", PopupTab::Images, state.active_tab),
9193
tab_button("Volumes", PopupTab::Volumes, state.active_tab),
94+
tab_button("Ports", PopupTab::Ports, state.active_tab),
9295
tab_button("Settings", PopupTab::Settings, state.active_tab),
9396
widget::text("").width(Length::Fill),
9497
toolbar,

‎src/app/popup/widgets.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,30 @@ pub(super) fn remove_confirm_row<'a>(prompt: String) -> Element<'a, Message> {
212212
.into()
213213
}
214214

215+
/// Kill prompt with both signals: `Kill` sends SIGTERM, `Force` sends SIGKILL.
216+
pub(super) fn kill_confirm_row<'a>(prompt: String) -> Element<'a, Message> {
217+
widget::container(
218+
widget::row![
219+
widget::text(prompt)
220+
.width(Length::FillPortion(FILL_PORTION_PROMPT))
221+
.size(FONT_PRIMARY),
222+
widget::button::destructive("Kill")
223+
.on_press(Message::ConfirmKill { force: false })
224+
.padding(PAD_CONFIRM_BUTTON),
225+
widget::button::destructive("Force")
226+
.on_press(Message::ConfirmKill { force: true })
227+
.padding(PAD_CONFIRM_BUTTON),
228+
widget::button::text("No")
229+
.on_press(Message::CancelKill)
230+
.padding(PAD_CONFIRM_BUTTON),
231+
]
232+
.align_y(Alignment::Center)
233+
.spacing(SPACING_SM),
234+
)
235+
.padding(PAD_LIST_ROW)
236+
.into()
237+
}
238+
215239
pub(super) fn prune_confirm_row<'a>(prompt: &'static str) -> Element<'a, Message> {
216240
widget::container(
217241
widget::row![

0 commit comments

Comments
 (0)