Add Perforce-style command Log panel + UI scale setting

Backend emits a p4-log event per command (cmd + item count + status).
Frontend adds a bottom Log panel toggled from a new Window menu (Ctrl+L),
an always-on status bar showing the last command, and a hover log-peek
popover over the activity indicator. Settings gains an interface-scale
(zoom) control (50–200%, persisted). All new strings translated (5 langs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-07 14:28:01 +03:00
parent 46a3be4489
commit 80f2acf40d
4 changed files with 191 additions and 16 deletions

View File

@ -4,8 +4,28 @@
use serde_json::Value;
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use tauri::State;
use std::sync::{Mutex, OnceLock};
use tauri::{AppHandle, Emitter, State};
/// App handle stashed at startup so the low-level p4 helpers can emit a live
/// command log to the frontend (a Perforce-style "Log" panel).
static APP: OnceLock<AppHandle> = OnceLock::new();
/// Emit one p4-log entry to the UI: the command line, how many items/lines it
/// returned, whether it succeeded, and any error text.
fn log_p4(args: &[&str], count: i64, ok: bool, err: Option<&str>) {
if let Some(app) = APP.get() {
let _ = app.emit(
"p4-log",
serde_json::json!({
"cmd": format!("p4 {}", args.join(" ")),
"count": count,
"ok": ok,
"err": err,
}),
);
}
}
#[derive(Default, Clone)]
struct Conn {
@ -43,10 +63,14 @@ fn p4_cmd(conn: &Conn, tagged: bool) -> Command {
/// Run a tagged (JSON) p4 command and collect the objects it prints.
fn run_json(conn: &Conn, args: &[&str]) -> Result<Vec<Value>, String> {
let out = p4_cmd(conn, true)
.args(args)
.output()
.map_err(|e| format!("Failed to start p4: {e}"))?;
let out = match p4_cmd(conn, true).args(args).output() {
Ok(o) => o,
Err(e) => {
let m = format!("Failed to start p4: {e}");
log_p4(args, 0, false, Some(&m));
return Err(m);
}
};
let text = String::from_utf8_lossy(&out.stdout);
let mut items = Vec::new();
@ -69,6 +93,7 @@ fn run_json(conn: &Conn, args: &[&str]) -> Result<Vec<Value>, String> {
.unwrap_or("unknown p4 error")
.trim()
.to_string();
log_p4(args, 0, false, Some(&msg));
return Err(msg);
}
// informational message (e.g. "File(s) not opened."), not a data record — skip
@ -82,9 +107,11 @@ fn run_json(conn: &Conn, args: &[&str]) -> Result<Vec<Value>, String> {
if items.is_empty() && !out.status.success() {
let e = String::from_utf8_lossy(&out.stderr).trim().to_string();
if !e.is_empty() {
log_p4(args, 0, false, Some(&e));
return Err(e);
}
}
log_p4(args, items.len() as i64, true, None);
Ok(items)
}
@ -279,15 +306,21 @@ async fn p4_disconnect(state: State<'_, AppState>) -> Result<(), String> {
/// Run a non-tagged p4 command (raw text out). Errors carry stderr.
fn run_text(conn: &Conn, args: &[&str]) -> Result<String, String> {
let out = p4_cmd(conn, false)
.args(args)
.output()
.map_err(|e| format!("Failed to start p4: {e}"))?;
let out = match p4_cmd(conn, false).args(args).output() {
Ok(o) => o,
Err(e) => {
let m = format!("Failed to start p4: {e}");
log_p4(args, 0, false, Some(&m));
return Err(m);
}
};
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
if !out.status.success() && !stderr.is_empty() {
log_p4(args, 0, false, Some(&stderr));
return Err(stderr);
}
log_p4(args, stdout.lines().count() as i64, true, None);
Ok(if stdout.is_empty() { stderr } else { stdout })
}
@ -568,6 +601,11 @@ pub fn run() {
.plugin(tauri_plugin_process::init());
}
builder
.setup(|app| {
// stash the app handle so p4 helpers can emit the live command log
let _ = APP.set(app.handle().clone());
Ok(())
})
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![
p4_clients,