|
1 | | -use gtk::gdk::RGBA; |
2 | | -use gtk::gio::ApplicationFlags; |
| 1 | +use gio::prelude::*; |
3 | 2 | use gtk::prelude::*; |
4 | | -use gtk::{Application, ApplicationWindow, Button, CssProvider, HeaderBar, Label, Notebook, Orientation, ScrolledWindow}; |
5 | | -use vte::{PtyFlags, Terminal}; |
6 | | -use vte::prelude::{TerminalExt, TerminalExtManual}; |
7 | | -use webkitgtk6::{WebContext, WebView}; |
8 | | -use webkitgtk6::prelude::WebViewExt; |
9 | | -use which::which; |
10 | | -use gio::Cancellable; |
| 3 | +use gio::ApplicationFlags; |
| 4 | +use gtk::Application; |
| 5 | + |
| 6 | +mod style; |
| 7 | +mod ui; |
| 8 | +mod terminal; |
| 9 | +mod webview; |
| 10 | + |
| 11 | +use style::apply_styles; |
| 12 | +use ui::build_ui; |
11 | 13 |
|
12 | 14 | fn main() { |
13 | | - // Initialize GTK |
14 | | - let app = Application::new(Some("com.example.rustterminal"), ApplicationFlags::default()); |
| 15 | + // Initialize GTK application |
| 16 | + let app = Application::new(Some("com.example.hackerterm"), ApplicationFlags::default()); |
| 17 | + |
| 18 | + // Connect startup to apply global settings and styles |
15 | 19 | app.connect_startup(|_| { |
16 | | - // Set dark theme globally |
| 20 | + // Enable dark theme |
17 | 21 | let settings = gtk::Settings::default().unwrap(); |
18 | 22 | settings.set_property("gtk-application-prefer-dark-theme", &true.to_value()); |
19 | | - settings.set_property("gtk-theme-name", &"Adwaita".to_value()); // Adwaita has dark variant |
20 | | - // Load custom CSS for semi-transparent background and styling |
21 | | - let provider = CssProvider::new(); |
22 | | - provider.load_from_data(" |
23 | | - window { |
24 | | - background-color: rgba(0, 0, 0, 0.8); /* Semi-transparent dark background */ |
25 | | - } |
26 | | - notebook { |
27 | | - background-color: transparent; |
28 | | - } |
29 | | - scrolledwindow { |
30 | | - background-color: transparent; |
31 | | - } |
32 | | - vte-terminal { |
33 | | - background-color: transparent; |
34 | | - color: #ffffff; |
35 | | - font-family: monospace; |
36 | | - font-size: 12pt; |
37 | | - } |
38 | | - "); |
39 | | - gtk::style_context_add_provider_for_display( |
40 | | - >k::gdk::Display::default().unwrap(), |
41 | | - &provider, |
42 | | - gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, |
43 | | - ); |
44 | | - }); |
45 | | - app.connect_activate(build_ui); |
46 | | - app.run(); |
47 | | -} |
| 23 | + settings.set_property("gtk-theme-name", &"Adwaita".to_value()); // Use Adwaita dark variant |
48 | 24 |
|
49 | | -fn build_ui(app: &Application) { |
50 | | - let window = ApplicationWindow::builder() |
51 | | - .application(app) |
52 | | - .title("Rust Terminal") |
53 | | - .default_width(800) |
54 | | - .default_height(600) |
55 | | - .build(); |
56 | | - // Create header bar |
57 | | - let header = HeaderBar::new(); |
58 | | - header.set_show_title_buttons(true); |
59 | | - window.set_titlebar(Some(&header)); |
60 | | - // Create notebook for tabs |
61 | | - let notebook = Notebook::new(); |
62 | | - notebook.set_tab_pos(gtk::PositionType::Top); |
63 | | - notebook.set_scrollable(true); |
64 | | - // Add button to header to create new tab |
65 | | - let add_button = Button::with_label("+"); |
66 | | - header.pack_start(&add_button); |
67 | | - let notebook_weak = notebook.downgrade(); |
68 | | - add_button.connect_clicked(move |_| { |
69 | | - if let Some(notebook) = notebook_weak.upgrade() { |
70 | | - add_tab(¬ebook); |
71 | | - } |
| 25 | + // Apply custom styles |
| 26 | + apply_styles(); |
72 | 27 | }); |
73 | | - // Add initial tab |
74 | | - add_tab(¬ebook); |
75 | | - window.set_child(Some(¬ebook)); |
76 | | - window.present(); |
77 | | -} |
78 | 28 |
|
79 | | -fn add_tab(notebook: &Notebook) { |
80 | | - // Create overlay for terminal and webview |
81 | | - let overlay = gtk::Overlay::new(); |
82 | | - // Create VTE Terminal |
83 | | - let terminal = Terminal::new(); |
84 | | - terminal.set_hexpand(true); |
85 | | - terminal.set_vexpand(true); |
86 | | - terminal.set_allow_hyperlink(true); |
87 | | - // Determine shell: prefer zsh if available, fallback to bash |
88 | | - let shell = if which("zsh").is_ok() { |
89 | | - "/bin/zsh".to_string() |
90 | | - } else { |
91 | | - "/bin/bash".to_string() |
92 | | - }; |
93 | | - // Spawn the shell in the terminal |
94 | | - terminal.spawn_async( |
95 | | - PtyFlags::DEFAULT, |
96 | | - None, |
97 | | - &[&shell], |
98 | | - &[], |
99 | | - glib::SpawnFlags::DEFAULT, |
100 | | - || {}, |
101 | | - -1, |
102 | | - None::<&Cancellable>, |
103 | | - |_| {}, |
104 | | - ); |
105 | | - overlay.set_child(Some(&terminal)); |
106 | | - // Create WebView for animations (transparent overlay) |
107 | | - let context = WebContext::default().unwrap(); |
108 | | - let webview = WebView::builder().web_context(&context).build(); |
109 | | - webview.set_background_color(&RGBA::new(0.0, 0.0, 0.0, 0.0)); // Fully transparent |
110 | | - // Load HTML with canvas and JavaScript for particle animations (simulating Hyperpower) |
111 | | - let html = r#" |
112 | | - <html> |
113 | | - <head> |
114 | | - <style> |
115 | | - body, html { |
116 | | - margin: 0; |
117 | | - padding: 0; |
118 | | - overflow: hidden; |
119 | | - background: transparent; } |
120 | | - canvas { display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* Allow clicks to pass through */ } |
121 | | - </style> |
122 | | - </head> |
123 | | - <body> |
124 | | - <canvas id="canvas"></canvas> |
125 | | - <script> |
126 | | - const canvas = document.getElementById('canvas'); |
127 | | - const ctx = canvas.getContext('2d'); |
128 | | - let particles = []; |
129 | | - let animationFrameId; |
130 | | - function resizeCanvas() { |
131 | | - canvas.width = window.innerWidth; |
132 | | - canvas.height = window.innerHeight; |
133 | | - } |
134 | | - window.addEventListener('resize', resizeCanvas); |
135 | | - resizeCanvas(); |
136 | | - class Particle { |
137 | | - constructor(x, y) { |
138 | | - this.x = x; |
139 | | - this.y = y; |
140 | | - this.size = Math.random() * 5 + 2; |
141 | | - this.speedX = Math.random() * 4 - 2; |
142 | | - this.speedY = Math.random() * 4 - 2; |
143 | | - this.color = `rgba(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255}, ${Math.random() * 0.5 + 0.5})`; |
144 | | - this.life = 30 + Math.random() * 20; |
145 | | - } |
146 | | - update() { |
147 | | - this.x += this.speedX; |
148 | | - this.y += this.speedY; |
149 | | - this.speedY += 0.1; // Gravity effect |
150 | | - this.life -= 1; |
151 | | - if (this.size > 0.2) this.size -= 0.1; |
152 | | - } |
153 | | - draw() { |
154 | | - ctx.fillStyle = this.color; |
155 | | - ctx.beginPath(); |
156 | | - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); |
157 | | - ctx.fill(); |
158 | | - } |
159 | | - } |
160 | | - function animate() { |
161 | | - ctx.clearRect(0, 0, canvas.width, canvas.height); |
162 | | - particles = particles.filter(particle => { |
163 | | - particle.update(); |
164 | | - particle.draw(); |
165 | | - return particle.life > 0; |
166 | | - }); |
167 | | - animationFrameId = requestAnimationFrame(animate); |
168 | | - } |
169 | | - animate(); |
170 | | - // Function to spawn particles (called from Rust on input) |
171 | | - function spawnParticles(count = 50) { |
172 | | - const x = Math.random() * canvas.width; |
173 | | - const y = Math.random() * canvas.height; |
174 | | - for (let i = 0; i < count; i++) { |
175 | | - particles.push(new Particle(x, y)); |
176 | | - } |
177 | | - } |
178 | | - </script> |
179 | | - </body> |
180 | | - </html> |
181 | | - "#; |
182 | | - webview.load_html(html, None); |
183 | | - // Make webview expand and overlay |
184 | | - webview.set_hexpand(true); |
185 | | - webview.set_vexpand(true); |
186 | | - overlay.add_overlay(&webview); |
187 | | - webview.set_sensitive(false); |
188 | | - webview.set_can_focus(false); |
189 | | - // Connect to VTE commit signal to trigger particles on text input |
190 | | - let webview_clone = webview.clone(); |
191 | | - terminal.connect_commit(move |_, text, _| { |
192 | | - if !text.is_empty() { |
193 | | - // Trigger JavaScript to spawn particles |
194 | | - webview_clone.evaluate_javascript("spawnParticles(50);", None, None, None::<&Cancellable>, |_| {}); |
195 | | - } |
196 | | - }); |
197 | | - // Wrap in ScrolledWindow for better handling |
198 | | - let scrolled = ScrolledWindow::new(); |
199 | | - scrolled.set_child(Some(&overlay)); |
200 | | - scrolled.set_hexpand(true); |
201 | | - scrolled.set_vexpand(true); |
202 | | - // Add to notebook with close button |
203 | | - let tab_box = gtk::Box::new(Orientation::Horizontal, 0); |
204 | | - let label = Label::new(Some("Terminal")); |
205 | | - tab_box.append(&label); |
206 | | - let close_button = Button::builder() |
207 | | - .icon_name("window-close-symbolic") |
208 | | - .css_classes(vec!["flat".to_string()]) |
209 | | - .build(); |
210 | | - tab_box.append(&close_button); |
211 | | - let _ = notebook.append_page(&scrolled, Some(&tab_box)); |
212 | | - let notebook_weak = notebook.downgrade(); |
213 | | - let scrolled_weak = scrolled.downgrade(); |
214 | | - close_button.connect_clicked(move |_| { |
215 | | - if let Some(notebook) = notebook_weak.upgrade() { |
216 | | - if let Some(scrolled) = scrolled_weak.upgrade() { |
217 | | - if let Some(page) = notebook.page_num(&scrolled) { |
218 | | - notebook.remove_page(Some(page)); |
219 | | - } |
220 | | - } |
221 | | - } |
222 | | - }); |
| 29 | + // Connect activate to build the UI |
| 30 | + app.connect_activate(build_ui); |
| 31 | + |
| 32 | + // Run the application |
| 33 | + app.run(); |
223 | 34 | } |
0 commit comments