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: 1 addition & 1 deletion Backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ env_logger = "0.11"
parking_lot = "0.12"
toml = "0.9.5"
directories = "6"

serde_json = "1.0"
2 changes: 1 addition & 1 deletion Backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn run() {
println!("Running OpenVCS...");

tauri::Builder::default()
.manage(state::AppState::default())
.manage(state::AppState::new_with_config())
.setup(|app| {
menus::build_and_attach_menu(app)?;
Ok(())
Expand Down
9 changes: 8 additions & 1 deletion Backend/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl Default for General {
checks_on_launch: true,
telemetry: false,
crash_reports: false,
}
}
}
}

Expand Down Expand Up @@ -207,6 +207,8 @@ pub struct Ux {
#[serde(default)] pub font_mono: String,
#[serde(default)] pub vim_nav: bool,
#[serde(default)] pub color_blind_mode: ColorBlindMode,
/// Max number of recent repositories to keep in MRU list
#[serde(default)] pub recents_limit: u32,
}
impl Default for Ux {
fn default() -> Self {
Expand All @@ -215,6 +217,7 @@ impl Default for Ux {
font_mono: "monospace".into(),
vim_nav: false,
color_blind_mode: ColorBlindMode::None,
recents_limit: 10,
}
}
}
Expand Down Expand Up @@ -424,6 +427,7 @@ impl AppConfig {
1 => { /* current */ }
_ => { /* future: add stepwise migrations */ }
}
// no-op
}

/// Clamp and normalize values so hand edits can’t break the app.
Expand All @@ -450,5 +454,8 @@ impl AppConfig {
self.network.http_low_speed_time_secs.clamp(1, 600);
self.network.http_low_speed_limit =
self.network.http_low_speed_limit.clamp(128, 10_000_000);

// UX
self.ux.recents_limit = self.ux.recents_limit.clamp(1, 100);
}
}
99 changes: 93 additions & 6 deletions Backend/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::{fs, io};
use std::{path::PathBuf, sync::Arc};

use log::{debug, info};
Expand All @@ -6,6 +7,11 @@ use parking_lot::RwLock;
use openvcs_core::Repo;
use crate::settings::AppConfig;
use crate::repo_settings::RepoConfig;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

// Default MRU size used as a fallback when settings are missing/invalid
pub const MAX_RECENTS: usize = 10;

/// Central application state.
/// Keeps track of the currently open repo and MRU recents.
Expand All @@ -28,11 +34,16 @@ pub struct AppState {
impl AppState {
pub fn new_with_config() -> Self {
let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf
Self {
let mut s = Self {
config: RwLock::new(cfg),
repo_config: RwLock::new(RepoConfig::default()),
..Default::default()
};
// Attempt to load recents from app data (not config dir)
if let Ok(list) = load_recents_from_disk() {
*s.recents.write() = list;
}
s
}

/// Persist current config to disk.
Expand Down Expand Up @@ -63,6 +74,7 @@ impl AppState {
next.validate();
next.save().map_err(|e| e.to_string())?;
*self.config.write() = next;
self.enforce_recents_limit_and_persist();
Ok(())
}

Expand Down Expand Up @@ -90,6 +102,7 @@ impl AppState {
next.validate();
next.save().map_err(|e| e.to_string())?;
*self.config.write() = next;
self.enforce_recents_limit_and_persist();
Ok(())
}

Expand All @@ -110,14 +123,13 @@ impl AppState {

*self.current_repo.write() = Some(repo);

// Update recents (front insert, unique, cap N)
// Update recents (front insert, unique, cap N from settings)
let mut r = self.recents.write();
r.retain(|p| p != &path);
r.insert(0, path.clone());
const MAX_RECENTS: usize = 10;
if r.len() > MAX_RECENTS {
r.truncate(MAX_RECENTS);
}
let limit = self.config.read().ux.recents_limit as usize;
let max_items = if limit == 0 { MAX_RECENTS } else { limit };
if r.len() > max_items { r.truncate(max_items); }

debug!(
"AppState: recents -> [{}]",
Expand All @@ -126,6 +138,11 @@ impl AppState {
.collect::<Vec<_>>()
.join(", ")
);

// Persist recents; ignore failures but log
if let Err(e) = save_recents_to_disk(&r.clone()) { // clone small vec
log::warn!("AppState: failed to persist recents: {}", e);
}
}

pub fn clear_current_repo(&self) {
Expand All @@ -143,3 +160,73 @@ impl AppState {
self.recents.read().clone()
}
}

// ──────────────────────────────────────────────────────────────────────────────
// Recents persistence (outside config dir)
// File format: JSON array of objects { "path": "..." } for forward compatibility.
// ──────────────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
struct RecentFileEntry { path: String }

fn recents_file_path() -> PathBuf {
if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") {
pd.data_dir().join("recents.json")
} else {
PathBuf::from("recents.json")
}
}

fn load_recents_from_disk() -> Result<Vec<PathBuf>, String> {
let p = recents_file_path();
let data = match fs::read_to_string(&p) {
Ok(s) => s,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(vec![]),
Err(e) => return Err(format!("read recents: {}", e)),
};

// Accept: [ { path }, ... ] or ["/path", ...]
let mut out: Vec<PathBuf> = Vec::new();
match serde_json::from_str::<serde_json::Value>(&data) {
Ok(serde_json::Value::Array(items)) => {
for it in items {
match it {
serde_json::Value::String(s) => {
if !s.trim().is_empty() { out.push(PathBuf::from(s)); }
}
serde_json::Value::Object(map) => {
if let Some(serde_json::Value::String(s)) = map.get("path") {
if !s.trim().is_empty() { out.push(PathBuf::from(s)); }
}
}
_ => {}
}
}
}
_ => {}
}
Ok(out)
}

fn save_recents_to_disk(list: &Vec<PathBuf>) -> Result<(), String> {
let p = recents_file_path();
if let Some(parent) = p.parent() { fs::create_dir_all(parent).map_err(|e| e.to_string())?; }
let entries: Vec<RecentFileEntry> = list
.iter()
.map(|pb| RecentFileEntry { path: pb.to_string_lossy().to_string() })
.collect();
let json = serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())?;
fs::write(&p, json).map_err(|e| e.to_string())
}

impl AppState {
fn enforce_recents_limit_and_persist(&self) {
let limit = self.config.read().ux.recents_limit as usize;
let max_items = if limit == 0 { MAX_RECENTS } else { limit };
let mut r = self.recents.write();
if r.len() > max_items { r.truncate(max_items); }
if let Err(e) = save_recents_to_disk(&r.clone()) {
log::warn!("AppState: failed to persist recents after settings change: {}", e);
}
}
}
14 changes: 12 additions & 2 deletions Backend/src/tauri_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,19 @@ pub fn current_repo_path(state: State<'_, AppState>) -> Option<String> {
.map(|repo| repo.inner().workdir().to_string_lossy().to_string())
}

#[derive(serde::Serialize)]
pub struct RecentRepoDto { path: String, name: Option<String> }

#[tauri::command]
pub fn list_recent_repos(state: State<'_, AppState>) -> Vec<String> {
state.recents().into_iter().map(|p| p.to_string_lossy().to_string()).collect()
pub fn list_recent_repos(state: State<'_, AppState>) -> Vec<RecentRepoDto> {
state
.recents()
.into_iter()
.map(|p| {
let name = p.file_name().and_then(|os| os.to_str()).map(|s| s.to_string());
RecentRepoDto { path: p.to_string_lossy().to_string(), name }
})
.collect()
}

/* ---------- helpers ---------- */
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Frontend/src/modals/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
Check for updates on launch
</label>
</div>

</form>

<!-- Git -->
Expand Down Expand Up @@ -205,6 +206,10 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
<option value="tritanopia">Tritanopia</option>
</select>
</div>
<div class="group">
<label for="set-recents-limit">Recent repositories to keep</label>
<input id="set-recents-limit" type="number" min="1" max="100" value="10" />
</div>
</form>

<!-- Footer actions -->
Expand All @@ -217,4 +222,4 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
</div>
</section>
</div>
</div>
</div>
6 changes: 5 additions & 1 deletion Frontend/src/scripts/features/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function wireSettings() {
cur.diff = { tab_width: 4, ignore_whitespace: 'none', max_file_size_mb: 10, intraline: true, show_binary_placeholders: true, external_diff: {enabled:false,path:'',args:''}, external_merge: {enabled:false,path:'',args:''}, binary_exts: ['png','jpg','dds','uasset'] };
cur.lfs = { enabled: true, concurrency: 4, bandwidth_kbps: 0, require_lock_before_edit: false, background_fetch_on_checkout: true };
cur.performance = { graph_node_cap: 5000, progressive_render: true, gpu_accel: true, index_warm_on_open: true, background_index_on_battery: false };
cur.ux = { ui_scale: 1.0, font_mono: 'monospace', vim_nav: false, color_blind_mode: 'none' };
cur.ux = { ui_scale: 1.0, font_mono: 'monospace', vim_nav: false, color_blind_mode: 'none', recents_limit: 10 };

await TAURI.invoke('set_global_settings', { cfg: cur });
await loadSettingsIntoForm(modal);
Expand Down Expand Up @@ -130,12 +130,15 @@ function collectSettingsFromForm(root: HTMLElement): GlobalSettings {
background_index_on_battery: !!get<HTMLInputElement>('#set-bg-index-on-battery')?.checked,
};

const rlRaw = get<HTMLInputElement>('#set-recents-limit')?.value ?? '';
const recentsLimit = rlRaw.trim() === '' ? 10 : Math.max(1, Math.min(100, Number(rlRaw)));
o.ux = {
...o.ux,
ui_scale: Number(get<HTMLInputElement>('#set-ui-scale')?.value ?? 1),
font_mono: get<HTMLInputElement>('#set-font-mono')?.value,
vim_nav: !!get<HTMLInputElement>('#set-vim-nav')?.checked,
color_blind_mode: get<HTMLSelectElement>('#set-cb-mode')?.value,
recents_limit: recentsLimit,
};

return o;
Expand All @@ -155,6 +158,7 @@ export async function loadSettingsIntoForm(root?: HTMLElement) {
const elChan = get<HTMLSelectElement>('#set-update-channel'); if (elChan) elChan.value = toKebab(cfg.general?.update_channel);
const elReo = get<HTMLInputElement>('#set-reopen-last'); if (elReo) elReo.checked = !!cfg.general?.reopen_last_repos;
const elChk = get<HTMLInputElement>('#set-checks-on-launch'); if (elChk) elChk.checked = !!cfg.general?.checks_on_launch;
const elRl = get<HTMLInputElement>('#set-recents-limit'); if (elRl) elRl.value = String(cfg.ux?.recents_limit ?? 10);

const backend = toKebab(cfg.git?.backend);
const elGb = get<HTMLSelectElement>('#set-git-backend'); if (elGb) elGb.value = backend === 'libgit2' ? 'libgit2' : 'git-system';
Expand Down
1 change: 1 addition & 0 deletions Frontend/src/scripts/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface GlobalSettings {
font_mono?: string;
vim_nav?: boolean;
color_blind_mode?: string;
recents_limit?: number;
};
}

Expand Down
Loading