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
7 changes: 0 additions & 7 deletions src/bin/test.rs

This file was deleted.

101 changes: 63 additions & 38 deletions src/bin/vimit-gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::thread;
use std::time::{Duration, Instant};
use tray_icon::{
Icon, MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent,
menu::{Menu, MenuEvent, MenuItem},
menu::{Menu, MenuEvent, MenuId, MenuItem},
};

use vimit as ng;
Expand Down Expand Up @@ -342,6 +342,52 @@ fn create_status_icon(color: (u8, u8, u8)) -> Icon {
Icon::from_rgba(rgba, width as u32, height as u32).expect("failed to create tray icon")
}

#[derive(Clone)]
struct TrayIds {
show: MenuId,
mini: MenuId,
quit: MenuId,
}

fn try_init_tray() -> Option<TrayIds> {
let tray_menu = Menu::new();
let show_item = MenuItem::new("Показать окно", true, None);
let mini_item = MenuItem::new("Мини-окно", true, None);
let quit_item = MenuItem::new("Выход", true, None);

for item in [&show_item, &mini_item, &quit_item] {
if let Err(error) = tray_menu.append(item) {
eprintln!("vimit-gui: tray menu unavailable: {error}");
return None;
}
}

let ids = TrayIds {
show: show_item.id().clone(),
mini: mini_item.id().clone(),
quit: quit_item.id().clone(),
};

let tray_icon_instance = match TrayIconBuilder::new()
.with_menu(Box::new(tray_menu))
.with_tooltip("VibeMode Control")
.with_icon(create_status_icon((141, 150, 170)))
.build()
{
Ok(icon) => icon,
Err(error) => {
eprintln!("vimit-gui: tray unavailable: {error}");
return None;
}
};

TRAY_ICON.with(|cell| {
*cell.borrow_mut() = Some(tray_icon_instance);
});

Some(ids)
}

fn spawn_mini_overlay(force_demo: bool) -> Result<(), String> {
let current =
std::env::current_exe().map_err(|error| format!("cannot find GUI exe: {error}"))?;
Expand Down Expand Up @@ -560,37 +606,17 @@ fn main() {
set_overlay_window_size(&app, is_compact);
}

// Initialize System Tray
let tray_menu = Menu::new();
let show_item = MenuItem::new("Показать окно", true, None);
let mini_item = MenuItem::new("Мини-окно", true, None);
let quit_item = MenuItem::new("Выход", true, None);
tray_menu.append(&show_item).unwrap();
tray_menu.append(&mini_item).unwrap();
tray_menu.append(&quit_item).unwrap();

let show_id = show_item.id().clone();
let mini_id = mini_item.id().clone();
let quit_id = quit_item.id().clone();

let tray_icon_instance = TrayIconBuilder::new()
.with_menu(Box::new(tray_menu))
.with_tooltip("VibeMode Control")
.with_icon(create_status_icon((141, 150, 170))) // grey by default
.build()
.unwrap();

TRAY_ICON.with(|cell| {
*cell.borrow_mut() = Some(tray_icon_instance);
});
let tray_ids = try_init_tray();
let has_tray = tray_ids.is_some();

// Minimize to tray on close request
let weak = app.as_weak();
app.window().on_close_requested(move || {
if let Some(app) = weak.upgrade() {
if has_tray && let Some(app) = weak.upgrade() {
let _ = app.hide();
return slint::CloseRequestResponse::KeepWindowShown;
}
slint::CloseRequestResponse::KeepWindowShown
slint::CloseRequestResponse::HideWindow
});

app.on_close_overlay(move || {
Expand Down Expand Up @@ -684,20 +710,19 @@ fn main() {
// Listen for Tray Events
let tray_timer = Timer::default();
let weak = app.as_weak();
let show_id_clone = show_id.clone();
let mini_id_clone = mini_id.clone();
let quit_id_clone = quit_id.clone();
tray_timer.start(TimerMode::Repeated, Duration::from_millis(100), move || {
if let (Ok(event), Some(app)) = (MenuEvent::receiver().try_recv(), weak.upgrade()) {
if event.id == show_id_clone {
let _ = app.show();
} else if event.id == mini_id_clone {
let demo = app.get_active_endpoint_label().as_str() == "demo";
if let Err(error) = spawn_mini_overlay(demo) {
app.set_error_text(error.into());
if let Some(ids) = tray_ids.as_ref() {
if let (Ok(event), Some(app)) = (MenuEvent::receiver().try_recv(), weak.upgrade()) {
if event.id == ids.show {
let _ = app.show();
} else if event.id == ids.mini {
let demo = app.get_active_endpoint_label().as_str() == "demo";
if let Err(error) = spawn_mini_overlay(demo) {
app.set_error_text(error.into());
}
} else if event.id == ids.quit {
let _ = slint::quit_event_loop();
}
} else if event.id == quit_id_clone {
let _ = slint::quit_event_loop();
}
}
if let (Ok(event), Some(app)) = (TrayIconEvent::receiver().try_recv(), weak.upgrade()) {
Expand Down
60 changes: 59 additions & 1 deletion src/cli/init.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate as vimit;
use std::io::{self, Write};
use std::path::Path;

use super::config::dirs_or_default;

Expand Down Expand Up @@ -79,7 +80,7 @@ pub fn run_init() -> Result<i32, String> {
let env_content = format!(
"VIBEMODE_API_KEY={key}\n# VIBEMODE_API_BASE=https://r-api.vibemod.pro\n"
);
std::fs::write(&env_file, env_content)
write_secret_env_file(&env_file, env_content.as_bytes())
.map_err(|e| format!("cannot write .env: {e}"))?;
println!(" .env created with VIBEMODE_API_KEY");
println!(" hint: you can also set the env var directly or use --api-key-env");
Expand Down Expand Up @@ -132,3 +133,60 @@ fn test_connection() -> Result<(), String> {
}
}
}

fn write_secret_env_file(path: &Path, content: &[u8]) -> std::io::Result<()> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);

#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}

let mut file = options.open(path)?;
file.write_all(content)
}

#[cfg(test)]
mod tests {
use super::write_secret_env_file;
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn secret_env_file_uses_create_new() {
let path = std::env::temp_dir().join(format!(
"vimit-init-create-new-{}.env",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));

write_secret_env_file(&path, b"VIBEMODE_API_KEY=test\n").unwrap();
let err = write_secret_env_file(&path, b"VIBEMODE_API_KEY=other\n").unwrap_err();

assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
let _ = std::fs::remove_file(path);
}

#[cfg(unix)]
#[test]
fn secret_env_file_is_owner_only_on_unix() {
use std::os::unix::fs::PermissionsExt;

let path = std::env::temp_dir().join(format!(
"vimit-init-owner-only-{}.env",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));

write_secret_env_file(&path, b"VIBEMODE_API_KEY=test\n").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;

assert_eq!(mode, 0o600);
let _ = std::fs::remove_file(path);
}
}
Loading