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
55 changes: 51 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Continuous Integration
name: CI

on:
push:
Expand All @@ -13,10 +13,10 @@ permissions:
contents: read

jobs:
check:
name: Build Check
build:
name: Build
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v7
Expand All @@ -37,3 +37,50 @@ jobs:

- name: Cargo Build
run: cargo build

lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt

- name: Auto-fix formatting
run: cargo fmt --all

- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "style: apply rustfmt [skip ci]"

clippy:
name: Clippy
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-4-dev libvte-2.91-gtk4-dev libadwaita-1-dev

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Rust Cache
uses: Swatinem/rust-cache@v2

- name: Clippy
run: cargo clippy -- -D warnings
9 changes: 9 additions & 0 deletions src/config_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,30 @@ pub async fn delete_keyring_password(alias: &str) -> anyhow::Result<()> {
Ok(())
}

pub const DEFAULT_TERMINAL_THEME: &str = "Dracula";

/// Global application configuration settings.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AppConfig {
pub monitor_refresh_rate: u32, // index: 0=1s, 1=3s, 2=5s, 3=10s
pub terminal_font: String,
pub terminal_scrollback: u32,
#[serde(default = "default_terminal_theme")]
pub terminal_theme: String,
pub confirm_tab_close: bool,
}

fn default_terminal_theme() -> String {
DEFAULT_TERMINAL_THEME.to_string()
}

impl Default for AppConfig {
fn default() -> Self {
Self {
monitor_refresh_rate: 1, // 3s
terminal_font: "Monospace 11".to_string(),
terminal_scrollback: 10000,
terminal_theme: default_terminal_theme(),
confirm_tab_close: false,
}
}
Expand Down
26 changes: 25 additions & 1 deletion src/ui/components/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub struct Settings {
}

impl Settings {
pub fn new() -> Self {
pub fn new(notebook: &gtk4::Notebook) -> Self {
let config = crate::config_observer::load_app_config().unwrap_or_else(|e| {
tracing::error!("Failed to load app config: {}", e);
AppConfig::default()
Expand Down Expand Up @@ -48,6 +48,18 @@ impl Settings {
let scrollback_spinner = gtk4::SpinButton::new(Some(&scrollback_adj), 1.0, 0);
scrollback_row.append(&scrollback_label); scrollback_row.append(&scrollback_spinner);
terminal_group.append(&scrollback_row);

let theme_labels: Vec<&str> = crate::ui::theme::THEMES.iter().map(|t| t.name).collect();
let theme_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
let theme_label = gtk4::Label::new(Some("Color Theme")); theme_label.set_hexpand(true); theme_label.set_halign(gtk4::Align::Start);
let theme_dropdown = gtk4::DropDown::from_strings(&theme_labels);
let current_theme_idx = crate::ui::theme::THEMES
.iter()
.position(|t| t.name == config.terminal_theme)
.unwrap_or(0) as u32;
theme_dropdown.set_selected(current_theme_idx);
theme_row.append(&theme_label); theme_row.append(&theme_dropdown);
terminal_group.append(&theme_row);
content.append(&terminal_group);

let monitor_group = gtk4::Box::new(gtk4::Orientation::Vertical, 12);
Expand Down Expand Up @@ -75,6 +87,7 @@ impl Settings {
let r_drop = refresh_dropdown.clone();
let f_btn = font_button.clone();
let s_spin = scrollback_spinner.clone();
let t_drop = theme_dropdown.clone();
let c_switch = confirm_switch.clone();

let save_config = move || {
Expand All @@ -84,6 +97,7 @@ impl Settings {
.map(|fd| fd.to_string())
.unwrap_or_else(|| "Monospace 11".to_string());
new_config.terminal_scrollback = s_spin.value() as u32;
new_config.terminal_theme = crate::ui::theme::theme_at(t_drop.selected() as usize).name.to_string();
new_config.confirm_tab_close = c_switch.is_active();
let _ = crate::config_observer::save_app_config(&new_config);
};
Expand All @@ -94,6 +108,16 @@ impl Settings {
let s3 = save_fn.clone(); scrollback_spinner.connect_value_changed(move |_| { s3(); });
let s5 = save_fn.clone(); confirm_switch.connect_active_notify(move |_| { s5(); });

let s4 = save_fn.clone();
let nb = notebook.clone();
theme_dropdown.connect_selected_notify(move |dd| {
s4();
crate::ui::theme::apply_to_open_terminals(
&nb,
crate::ui::theme::theme_at(dd.selected() as usize),
);
});

scrolled.set_child(Some(&content));
container.append(&scrolled);

Expand Down
1 change: 1 addition & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod style;
pub mod theme;
pub mod window;
pub mod server_list;
pub mod components;
Expand Down
155 changes: 155 additions & 0 deletions src/ui/theme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use gtk4::gdk::RGBA;
use std::str::FromStr;
use vte4::prelude::*;

/// A terminal color scheme: default foreground/background/cursor colors plus the
/// 16-entry ANSI palette (8 normal + 8 bright).
pub struct TerminalTheme {
pub name: &'static str,
pub foreground: &'static str,
pub background: &'static str,
pub cursor: &'static str,
pub palette: [&'static str; 16],
}

/// Built-in terminal themes. The first entry is used as the fallback default.
pub static THEMES: &[TerminalTheme] = &[
TerminalTheme {
name: "Dracula",
foreground: "#f8f8f2",
background: "#282a36",
cursor: "#f8f8f2",
palette: [
"#21222c", "#ff5555", "#50fa7b", "#f1fa8c", "#bd93f9", "#ff79c6", "#8be9fd", "#f8f8f2",
"#6272a4", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df", "#a4ffff", "#ffffff",
],
},
TerminalTheme {
name: "Nord",
foreground: "#d8dee9",
background: "#2e3440",
cursor: "#d8dee9",
palette: [
"#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0",
"#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4",
],
},
TerminalTheme {
name: "Tokyo Night",
foreground: "#c0caf5",
background: "#1a1b26",
cursor: "#c0caf5",
palette: [
"#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6",
"#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5",
],
},
TerminalTheme {
name: "Catppuccin Mocha",
foreground: "#cdd6f4",
background: "#1e1e2e",
cursor: "#f5e0dc",
palette: [
"#45475a", "#f38ba8", "#a6e3a1", "#f9e2af", "#89b4fa", "#f5c2e7", "#94e2d5", "#bac2de",
"#585b70", "#f38ba8", "#a6e3a1", "#f9e2af", "#89b4fa", "#f5c2e7", "#94e2d5", "#a6adc8",
],
},
TerminalTheme {
name: "Gruvbox Dark",
foreground: "#ebdbb2",
background: "#282828",
cursor: "#ebdbb2",
palette: [
"#282828", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#a89984",
"#928374", "#fb4934", "#b8bb26", "#fabd2f", "#83a598", "#d3869b", "#8ec07c", "#ebdbb2",
],
},
TerminalTheme {
name: "One Dark",
foreground: "#abb2bf",
background: "#282c34",
cursor: "#528bff",
palette: [
"#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf",
"#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff",
],
},
TerminalTheme {
name: "Solarized Dark",
foreground: "#839496",
background: "#002b36",
cursor: "#93a1a1",
palette: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
TerminalTheme {
name: "Monokai",
foreground: "#f8f8f2",
background: "#272822",
cursor: "#f8f8f0",
palette: [
"#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2",
"#75715e", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5",
],
},
TerminalTheme {
name: "Solarized Light",
foreground: "#657b83",
background: "#fdf6e3",
cursor: "#586e75",
palette: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
},
TerminalTheme {
name: "GitHub Light",
foreground: "#24292e",
background: "#ffffff",
cursor: "#24292e",
palette: [
"#24292e", "#d73a49", "#28a745", "#dbab09", "#0366d6", "#5a32a3", "#0598bc", "#6a737d",
"#959da5", "#cb2431", "#22863a", "#b08800", "#005cc5", "#5a32a3", "#3192aa", "#d1d5da",
],
},
];

pub fn get_theme(name: &str) -> &'static TerminalTheme {
THEMES.iter().find(|t| t.name == name).unwrap_or(&THEMES[0])
}

pub fn theme_at(index: usize) -> &'static TerminalTheme {
THEMES.get(index).unwrap_or(&THEMES[0])
}

pub fn apply_to_terminal(theme: &TerminalTheme, terminal: &vte4::Terminal) {
let fg = RGBA::from_str(theme.foreground).unwrap_or(RGBA::WHITE);
let bg = RGBA::from_str(theme.background).unwrap_or(RGBA::BLACK);
let cursor = RGBA::from_str(theme.cursor).unwrap_or(fg);
let mut palette = [RGBA::BLACK; 16];
for (i, hex) in theme.palette.iter().enumerate() {
palette[i] = RGBA::from_str(hex).unwrap_or(RGBA::BLACK);
}
let palette_refs: Vec<&RGBA> = palette.iter().collect();
terminal.set_colors(Some(&fg), Some(&bg), &palette_refs);
terminal.set_color_cursor(Some(&cursor));
}

pub fn apply_to_open_terminals(notebook: &gtk4::Notebook, theme: &TerminalTheme) {
for i in 0..notebook.n_pages() {
let Some(page) = notebook.nth_page(Some(i)) else { continue };
if !page.widget_name().starts_with("session:") {
continue;
}
let Some(bx) = page.downcast_ref::<gtk4::Box>() else { continue };
let mut child = bx.first_child();
while let Some(c) = child {
if let Some(term) = c.downcast_ref::<vte4::Terminal>() {
apply_to_terminal(theme, term);
}
child = c.next_sibling();
}
}
}
6 changes: 5 additions & 1 deletion src/ui/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl AppWindow {
sessions_box.append(&notebook);
stack.add_named(&sessions_box, Some("sessions"));

let settings = Settings::new();
let settings = Settings::new(&notebook);
stack.add_named(&settings.container, Some("settings"));

let keys_box = build_ssh_keys_ui(&window);
Expand Down Expand Up @@ -212,6 +212,10 @@ impl AppWindow {
let font_desc = gtk4::pango::FontDescription::from_string(&app_config.terminal_font);
terminal.set_font(Some(&font_desc));
terminal.set_scrollback_lines(app_config.terminal_scrollback as i64);
crate::ui::theme::apply_to_terminal(
crate::ui::theme::get_theme(&app_config.terminal_theme),
&terminal,
);

let key_controller = gtk4::EventControllerKey::new();
let terminal_clone = terminal.clone();
Expand Down