Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
builddir/
dist/
crates/aetheris-app/libs/
.codex/
graphify-out/
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,16 @@ Before finishing a change, check:
- warnings are not introduced;
- user-facing strings are concise and GNOME-style;
- docs and workflows describe behavior that is actually implemented.

## graphify

This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.

When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.

Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ members = ["crates/aetheris-app", "crates/aetheris-kube"]

[workspace.package]
version = "1.3.0"
edition = "2021"
edition = "2024"
license = "GPL-3.0-or-later"
authors = ["Leandro Rodrigues <leandromqrs@hotmail.com>"]

Expand Down
2 changes: 1 addition & 1 deletion crates/aetheris-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "aetheris-app"
description = "Aetheris native Kubernetes client for GNOME"
version = "1.3.0"
edition = "2021"
edition = "2024"
license = "GPL-3.0-or-later"
authors = ["Leandro Rodrigues <leandromqrs@hotmail.com>"]

Expand Down
2 changes: 1 addition & 1 deletion crates/aetheris-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use aetheris_kube::{
ObjectDetail, ObjectEvent, ObjectSummary, ObjectWatchEvent, PodExecEvent, PodExecRequest,
PodLogRequest, PodPortForwardEvent, PodPortForwardRequest, ResourceKind, ResourceUsage,
};
use futures::future::{AbortHandle, Abortable};
use futures::FutureExt;
use futures::future::{AbortHandle, Abortable};
use relm4::adw::prelude::*;
use relm4::prelude::*;
use relm4::{adw, gtk};
Expand Down
6 changes: 2 additions & 4 deletions crates/aetheris-app/src/app/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,13 @@ pub(super) async fn list_objects_snapshot(
resource: ResourceKind,
namespace: Option<String>,
) -> Result<Vec<ObjectSummary>, String> {
let result = async {
async {
let manager = KubeManager::load()?;
let session = manager.connect_context(&context).await?;
session.list_objects(&resource, namespace.as_deref()).await
}
.await
.map_err(format_error);

result
.map_err(format_error)
}

pub(super) async fn stream_object_watch(
Expand Down
20 changes: 10 additions & 10 deletions crates/aetheris-app/src/app/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,16 +363,16 @@ impl App {
self.show_custom_namespace_dialog(root);
return;
}
if let Some(namespace) = choices.get(index as usize) {
if self.selected_namespace != *namespace {
self.selected_namespace.clone_from(namespace);
self.remember_selected_namespace();
self.sync_dropdowns(Some(sender.clone()));
self.show_object_list();
self.stop_log_stream();
self.stop_port_forward();
self.refresh_objects(sender);
}
if let Some(namespace) = choices.get(index as usize)
&& self.selected_namespace != *namespace
{
self.selected_namespace.clone_from(namespace);
self.remember_selected_namespace();
self.sync_dropdowns(Some(sender.clone()));
self.show_object_list();
self.stop_log_stream();
self.stop_port_forward();
self.refresh_objects(sender);
}
}
AppMsg::CustomNamespaceEntered => {
Expand Down
11 changes: 5 additions & 6 deletions crates/aetheris-app/src/app/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,11 @@ impl App {
self.namespaces.push(namespace.to_owned());
}

if let Some(context) = self.selected_context.clone() {
if let Some(project) = self.projects.selected_project_mut() {
if project.add_custom_namespace(&context, namespace) {
self.save_projects_or_toast();
}
}
if let Some(context) = self.selected_context.clone()
&& let Some(project) = self.projects.selected_project_mut()
&& project.add_custom_namespace(&context, namespace)
{
self.save_projects_or_toast();
}
}

Expand Down
16 changes: 10 additions & 6 deletions crates/aetheris-app/src/app/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,9 +860,11 @@ mod tests {
project.custom_namespaces_for_context(Some("prod")),
vec![String::from("billing")]
);
assert!(project
.custom_namespaces_for_context(Some("stage"))
.is_empty());
assert!(
project
.custom_namespaces_for_context(Some("stage"))
.is_empty()
);
assert!(!project.has_custom_namespace(Some("stage"), "billing"));
}

Expand All @@ -876,9 +878,11 @@ mod tests {
project.add_custom_namespace("prod", "billing");

assert!(project.remove_custom_namespace("prod", "billing"));
assert!(project
.custom_namespaces_for_context(Some("prod"))
.is_empty());
assert!(
project
.custom_namespaces_for_context(Some("prod"))
.is_empty()
);
assert!(project.custom_namespaces_by_context.is_empty());
}

Expand Down
12 changes: 6 additions & 6 deletions crates/aetheris-app/src/app/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ fn app_style_candidates() -> Vec<std::path::PathBuf> {
paths.extend(std::env::split_paths(&data_dirs).map(|path| path.join(APP_STYLE_RESOURCE)));
}

if let Ok(exe) = std::env::current_exe() {
if let Some(bin_dir) = exe.parent() {
paths.push(bin_dir.join("../share").join(APP_STYLE_RESOURCE));
paths.push(bin_dir.join("share").join(APP_STYLE_RESOURCE));
paths.push(bin_dir.join("../style.css"));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(bin_dir) = exe.parent()
{
paths.push(bin_dir.join("../share").join(APP_STYLE_RESOURCE));
paths.push(bin_dir.join("share").join(APP_STYLE_RESOURCE));
paths.push(bin_dir.join("../style.css"));
}

if let Some(app_dir) = std::env::var_os("APPDIR") {
Expand Down
4 changes: 3 additions & 1 deletion crates/aetheris-app/src/app/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ pub(super) fn terminal_error_message(error: &str) -> String {
|| lower.contains("pods/exec")
|| (lower.contains("cannot create") && lower.contains("exec"))
{
return tr("You do not have permission to open a terminal for this Pod. Your Kubernetes user needs create access to pods/exec in this namespace.");
return tr(
"You do not have permission to open a terminal for this Pod. Your Kubernetes user needs create access to pods/exec in this namespace.",
);
}

tr_format(
Expand Down
50 changes: 24 additions & 26 deletions crates/aetheris-app/src/app/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,30 +107,28 @@ pub(super) fn namespace_selector_row(
.build();
action.add_prefix(&gtk::Image::from_icon_name("folder-symbolic"));

if is_custom {
if let Some(sender) = sender {
let edit_button = gtk::Button::from_icon_name("document-edit-symbolic");
edit_button.add_css_class("flat");
edit_button.set_valign(gtk::Align::Center);
edit_button.set_tooltip_text(Some(&tr("Rename")));
edit_button.connect_clicked({
let sender = sender.clone();
let namespace = namespace.to_owned();
move |_| sender.input(AppMsg::OpenRenameNamespaceDialog(namespace.clone()))
});
action.add_suffix(&edit_button);

let delete_button = gtk::Button::from_icon_name("user-trash-symbolic");
delete_button.add_css_class("flat");
delete_button.add_css_class("destructive-action");
delete_button.set_valign(gtk::Align::Center);
delete_button.set_tooltip_text(Some(&tr("Remove")));
delete_button.connect_clicked({
let namespace = namespace.to_owned();
move |_| sender.input(AppMsg::RemoveCustomNamespace(namespace.clone()))
});
action.add_suffix(&delete_button);
}
if is_custom && let Some(sender) = sender {
let edit_button = gtk::Button::from_icon_name("document-edit-symbolic");
edit_button.add_css_class("flat");
edit_button.set_valign(gtk::Align::Center);
edit_button.set_tooltip_text(Some(&tr("Rename")));
edit_button.connect_clicked({
let sender = sender.clone();
let namespace = namespace.to_owned();
move |_| sender.input(AppMsg::OpenRenameNamespaceDialog(namespace.clone()))
});
action.add_suffix(&edit_button);

let delete_button = gtk::Button::from_icon_name("user-trash-symbolic");
delete_button.add_css_class("flat");
delete_button.add_css_class("destructive-action");
delete_button.set_valign(gtk::Align::Center);
delete_button.set_tooltip_text(Some(&tr("Remove")));
delete_button.connect_clicked({
let namespace = namespace.to_owned();
move |_| sender.input(AppMsg::RemoveCustomNamespace(namespace.clone()))
});
action.add_suffix(&delete_button);
}

if selected {
Expand Down Expand Up @@ -602,8 +600,8 @@ const RELATED_POD_COLUMNS: [ObjectColumn; 3] = [
/// same cell factories as the main object table, default widths, no
/// persistence. Returns the sorted model too — activation positions are
/// indices into it, not into the unsorted store.
pub(super) fn related_pods_column_view(
) -> (gtk::ColumnView, gtk::gio::ListStore, gtk::SortListModel) {
pub(super) fn related_pods_column_view()
-> (gtk::ColumnView, gtk::gio::ListStore, gtk::SortListModel) {
let store = gtk::gio::ListStore::new::<gtk::glib::BoxedAnyObject>();
let view = gtk::ColumnView::builder()
.single_click_activate(true)
Expand Down
28 changes: 18 additions & 10 deletions crates/aetheris-app/src/app/yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,12 @@ fn update_yaml_error_state(buffer: &sourceview5::Buffer, error_label: &gtk::Labe
return;
};

if let Some(tag) = text_buffer.tag_table().lookup("yaml-error-line") {
if let Some(start) = text_buffer.iter_at_line((line - 1) as i32) {
let mut end = start;
end.forward_to_line_end();
text_buffer.apply_tag(&tag, &start, &end);
}
if let Some(tag) = text_buffer.tag_table().lookup("yaml-error-line")
&& let Some(start) = text_buffer.iter_at_line((line - 1) as i32)
{
let mut end = start;
end.forward_to_line_end();
text_buffer.apply_tag(&tag, &start, &end);
}
error_label.set_label(&tr_format(
"Line {line}: {message}",
Expand Down Expand Up @@ -579,7 +579,9 @@ pub(super) fn desired_state_explanation(kind: &str, mapping: &serde_yaml::Mappin
"ConfigMap" => {
tr("ConfigMaps usually express desired configuration through data instead of spec.")
}
"Secret" => tr("Secrets usually express desired sensitive data through data or stringData instead of spec."),
"Secret" => tr(
"Secrets usually express desired sensitive data through data or stringData instead of spec.",
),
_ => tr("This manifest does not include a spec section."),
};
}
Expand Down Expand Up @@ -863,14 +865,20 @@ pub(super) fn kind_explanation(kind: &str) -> String {

pub(super) fn spec_explanation(kind: &str) -> String {
match kind {
"Pod" => tr("Desired Pod configuration, including containers, volumes, restart policy and scheduling hints."),
"Deployment" => tr("Desired Deployment state, usually replicas, selector and the Pod template."),
"Pod" => tr(
"Desired Pod configuration, including containers, volumes, restart policy and scheduling hints.",
),
"Deployment" => {
tr("Desired Deployment state, usually replicas, selector and the Pod template.")
}
"Service" => tr("Desired Service routing, including selector, type and exposed ports."),
"Ingress" => tr("Desired routing rules, TLS configuration and backend Services."),
"Job" => tr("Desired one-shot workload, including completion policy and Pod template."),
"CronJob" => tr("Desired schedule and Job template."),
"Node" => tr("Node spec is mostly managed by Kubernetes and should be edited carefully."),
_ => tr("Desired state for this resource. Controllers reconcile the live object toward this section."),
_ => tr(
"Desired state for this resource. Controllers reconcile the live object toward this section.",
),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/aetheris-kube/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use kube::api::ListParams;
use kube::{Api, ResourceExt};

use crate::status::age_label;
use crate::{resource_scope, KubeSession, ObjectEvent, ResourceKind};
use crate::{KubeSession, ObjectEvent, ResourceKind, resource_scope};

impl KubeSession {
pub(crate) async fn object_events(
Expand Down
40 changes: 20 additions & 20 deletions crates/aetheris-kube/src/exec.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{bail, Context as AnyhowContext, Result};
use anyhow::{Context as AnyhowContext, Result, bail};
use k8s_openapi::api::core::v1::Pod;
use kube::api::AttachParams;
use kube::Api;
use kube::api::AttachParams;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};

use crate::{KubeSession, PodExecEvent, PodExecRequest};
Expand Down Expand Up @@ -70,15 +70,15 @@ impl KubeSession {
.await
.context("failed to finish exec session")?;

if let Some(status) = status {
if status.status.as_deref() == Some("Failure") {
bail!(
"{}",
status
.message
.unwrap_or_else(|| String::from("command failed"))
);
}
if let Some(status) = status
&& status.status.as_deref() == Some("Failure")
{
bail!(
"{}",
status
.message
.unwrap_or_else(|| String::from("command failed"))
);
}

Ok(())
Expand Down Expand Up @@ -155,15 +155,15 @@ impl KubeSession {
.await
.context("failed to finish terminal session")?;

if let Some(status) = status {
if status.status.as_deref() == Some("Failure") {
bail!(
"{}",
status
.message
.unwrap_or_else(|| String::from("terminal session failed"))
);
}
if let Some(status) = status
&& status.status.as_deref() == Some("Failure")
{
bail!(
"{}",
status
.message
.unwrap_or_else(|| String::from("terminal session failed"))
);
}

Ok(())
Expand Down
Loading
Loading