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
3 changes: 2 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ pub fn run(cli: Cli) -> anyhow::Result<()> {
return Ok(());
}

if cli.clear_session || cli.clear_tool_state || cli.session_info {
if cli.clear_session || cli.clear_tool_state || cli.session_info || cli.rename_session.is_some()
{
run_session_cli_commands(&cli)?;
return Ok(());
}
Expand Down
20 changes: 20 additions & 0 deletions src/app/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ use crate::cli::Cli;
use crate::env_vars::WAYLAND_DISPLAY_ENV;

pub(crate) fn run_session_cli_commands(cli: &Cli) -> anyhow::Result<()> {
if let Some(display_name) = cli.rename_session.as_deref() {
let raw_path = cli
.session_file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--rename-session requires --session-file"))?;
let raw = raw_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("--session-file path must be valid UTF-8"))?;
let path = crate::session::normalize_named_session_file_arg(raw);
match crate::session::catalog::rename_session_display_name_by_path(&path, display_name)? {
Some(entry) => {
println!("Renamed session to {}.", entry.display_name);
}
None => {
anyhow::bail!("session is not in the named-session catalog");
}
}
return Ok(());
}

let loaded = crate::config::Config::load()?;
// [session] configures which files these commands act on. A load that
// fell back to defaults for that section would silently retarget a
Expand Down
3 changes: 3 additions & 0 deletions src/app/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ pub(crate) fn print_usage() {
println!(" wayscriber --active --session-file PATH Use a named session file");
println!(" wayscriber --freeze --session-file PATH Use a named session file");
println!(" wayscriber --session-info [--session-file PATH] Inspect saved session data");
println!(
" wayscriber --rename-session NAME --session-file PATH Rename a catalog display name"
);
println!(" wayscriber --clear-session [--session-file PATH] Remove saved session data");
println!(
" wayscriber --clear-tool-state [--session-file PATH] Reset saved tool defaults only"
Expand Down
80 changes: 73 additions & 7 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub struct Cli {
/// Show session persistence status and file paths
pub session_info: bool,

/// Rename a named session's catalog display name (session files are untouched)
pub rename_session: Option<String>,

/// Use a named session file for active/freeze/info/clear operations
pub session_file: Option<PathBuf>,

Expand Down Expand Up @@ -132,6 +135,10 @@ impl Cli {
"--clear-session" => cli.clear_session = true,
"--clear-tool-state" => cli.clear_tool_state = true,
"--session-info" => cli.session_info = true,
"--rename-session" => {
index += 1;
cli.rename_session = Some(value_after(&args, index, "--rename-session")?);
}
"--session-file" => {
index += 1;
cli.session_file =
Expand All @@ -157,6 +164,9 @@ impl Cli {
cli.session_file =
Some(PathBuf::from(value_from_equals(arg, "--session-file")?));
}
_ if arg.starts_with("--rename-session=") => {
cli.rename_session = Some(value_from_equals(arg, "--rename-session")?);
}
_ if is_short_option_cluster(arg) => {
match parse_short_option_cluster(&args, index, &mut cli)? {
ShortOptionOutcome::Continue(next_index) => index = next_index,
Expand Down Expand Up @@ -214,6 +224,7 @@ impl Cli {
|| self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some()
|| self.session_file.is_some()
|| self.freeze
|| self.exit_after_capture
Expand All @@ -222,6 +233,29 @@ impl Cli {
|| self.no_resume_session
}

/// Whether an option belongs to an overlay launch or daemon interaction.
///
/// Catalog-only commands must reject these options because they return
/// before any overlay or daemon behavior can honor them.
fn selects_overlay_option(&self) -> bool {
self.daemon
|| self.daemon_toggle
|| self.daemon_action.is_some()
|| self.light_toggle
|| self.light_draw_toggle
|| self.light_draw_on
|| self.light_draw_off
|| self.active
|| self.mode.is_some()
|| self.no_tray
|| self.freeze_on_show
|| self.freeze
|| self.exit_after_capture
|| self.no_exit_after_capture
|| self.resume_session
|| self.no_resume_session
}

fn validate(&self) -> Result<(), String> {
if self.runtime_capabilities
&& (self.selects_a_launch_command() || self.about || self.check_update)
Expand All @@ -244,6 +278,22 @@ impl Cli {
if self.clear_tool_state && self.session_info {
return Err(conflict("--clear-tool-state", "--session-info"));
}
if self.rename_session.is_some() && self.session_info {
return Err(conflict("--rename-session", "--session-info"));
}
if self.rename_session.is_some() && self.clear_session {
return Err(conflict("--rename-session", "--clear-session"));
}
if self.rename_session.is_some() && self.clear_tool_state {
return Err(conflict("--rename-session", "--clear-tool-state"));
}

if self.rename_session.is_some() && self.session_file.is_none() {
return Err("--rename-session requires --session-file".to_string());
}
if self.rename_session.is_some() && self.selects_overlay_option() {
return Err("--rename-session conflicts with overlay/daemon options".to_string());
}

if self.freeze_on_show && !self.daemon {
return Err("--freeze-on-show requires --daemon".to_string());
Expand Down Expand Up @@ -282,10 +332,11 @@ impl Cli {
|| self.daemon_toggle
|| self.clear_session
|| self.clear_tool_state
|| self.session_info)
|| self.session_info
|| self.rename_session.is_some())
{
return Err(
"--session-file requires --active, --freeze, --daemon, --daemon-toggle, --session-info, --clear-session, or --clear-tool-state"
"--session-file requires --active, --freeze, --daemon, --daemon-toggle, --session-info, --clear-session, --clear-tool-state, or --rename-session"
.to_string(),
);
}
Expand All @@ -307,6 +358,7 @@ impl Cli {
|| self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some()
|| self.about)
{
return Err("--daemon-toggle conflicts with the selected command".to_string());
Expand All @@ -325,6 +377,7 @@ impl Cli {
|| self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some()
|| self.session_file.is_some()
|| self.freeze
|| self.exit_after_capture
Expand All @@ -346,22 +399,33 @@ impl Cli {
return Err("--session-info conflicts with --daemon/--active".to_string());
}
if self.freeze
&& (self.daemon || self.clear_session || self.clear_tool_state || self.session_info)
&& (self.daemon
|| self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some())
{
return Err("--freeze conflicts with the selected command".to_string());
}
if (self.clear_session || self.clear_tool_state || self.session_info) && self.resume_session
if (self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some())
&& self.resume_session
{
return Err(
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state"
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state/--rename-session"
.to_string(),
);
}
if (self.clear_session || self.clear_tool_state || self.session_info)
if (self.clear_session
|| self.clear_tool_state
|| self.session_info
|| self.rename_session.is_some())
&& self.no_resume_session
{
return Err(
"--no-resume-session conflicts with --clear-session/--session-info/--clear-tool-state"
"--no-resume-session conflicts with --clear-session/--session-info/--clear-tool-state/--rename-session"
.to_string(),
);
}
Expand Down Expand Up @@ -469,6 +533,7 @@ pub(crate) fn print_help() {
println!(" wayscriber --active --session-file PATH");
println!(" wayscriber --freeze [--session-file PATH]");
println!(" wayscriber --session-info [--session-file PATH]");
println!(" wayscriber --rename-session NAME --session-file PATH");
println!(" wayscriber --clear-session [--session-file PATH]");
println!(" wayscriber --clear-tool-state [--session-file PATH]");
println!(" wayscriber --about");
Expand All @@ -494,6 +559,7 @@ pub(crate) fn print_help() {
println!(" --clear-session Delete persisted session data and backups");
println!(" --clear-tool-state Remove saved tool defaults but keep boards");
println!(" --session-info Show session persistence status");
println!(" --rename-session NAME Rename a named session catalog label");
println!(" --session-file PATH Use a named session file");
println!(" --about Show the About window");
println!(" --check-update Check wayscriber.com for a newer release");
Expand Down
84 changes: 80 additions & 4 deletions src/cli/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ fn session_file_requires_supported_command() {
]);
assert_eq!(
result.unwrap_err(),
"--session-file requires --active, --freeze, --daemon, --daemon-toggle, --session-info, --clear-session, or --clear-tool-state"
"--session-file requires --active, --freeze, --daemon, --daemon-toggle, --session-info, --clear-session, --clear-tool-state, or --rename-session"
);
}

Expand Down Expand Up @@ -239,24 +239,100 @@ fn offline_session_commands_reject_resume_overrides() {
let info_result = Cli::try_parse_from(["wayscriber", "--session-info", "--resume-session"]);
assert_eq!(
info_result.unwrap_err(),
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state"
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state/--rename-session"
);

let clear_result =
Cli::try_parse_from(["wayscriber", "--clear-session", "--no-resume-session"]);
assert_eq!(
clear_result.unwrap_err(),
"--no-resume-session conflicts with --clear-session/--session-info/--clear-tool-state"
"--no-resume-session conflicts with --clear-session/--session-info/--clear-tool-state/--rename-session"
);

let tool_state_result =
Cli::try_parse_from(["wayscriber", "--clear-tool-state", "--resume-session"]);
assert_eq!(
tool_state_result.unwrap_err(),
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state"
"--resume-session conflicts with --clear-session/--session-info/--clear-tool-state/--rename-session"
);
}

#[test]
fn rename_session_requires_session_file_and_accepts_equals_form() {
let missing = Cli::try_parse_from(["wayscriber", "--rename-session", "Lecture"]);
assert_eq!(
missing.unwrap_err(),
"--rename-session requires --session-file"
);

let cli = parse_cli([
"wayscriber",
"--rename-session=Lecture 04",
"--session-file",
"/tmp/lecture.wayscriber-session",
]);
assert_eq!(cli.rename_session.as_deref(), Some("Lecture 04"));
assert_eq!(
cli.session_file,
Some(PathBuf::from("/tmp/lecture.wayscriber-session"))
);
}

#[test]
fn rename_session_conflicts_with_other_session_commands() {
let result = Cli::try_parse_from([
"wayscriber",
"--rename-session",
"Lecture",
"--session-file",
"/tmp/lecture.wayscriber-session",
"--session-info",
]);
assert_eq!(
result.unwrap_err(),
"--rename-session conflicts with --session-info"
);
}

#[test]
fn rename_session_rejects_every_overlay_option() {
let overlay_options = [
vec!["--daemon"],
vec!["--daemon-toggle"],
vec!["--daemon-action", "toggle_help"],
vec!["--light-toggle"],
vec!["--light-draw-toggle"],
vec!["--light-draw-on"],
vec!["--light-draw-off"],
vec!["--active"],
vec!["--mode", "whiteboard"],
vec!["--no-tray"],
vec!["--freeze-on-show"],
vec!["--freeze"],
vec!["--exit-after-capture"],
vec!["--no-exit-after-capture"],
vec!["--resume-session"],
vec!["--no-resume-session"],
];

for option in overlay_options {
let mut args = vec![
"wayscriber",
"--rename-session",
"Lecture",
"--session-file",
"/tmp/lecture.wayscriber-session",
];
args.extend(option.iter().copied());

assert_eq!(
Cli::try_parse_from(args).unwrap_err(),
"--rename-session conflicts with overlay/daemon options",
"expected rename with {option:?} to be rejected"
);
}
}

#[test]
fn offline_session_commands_conflict_with_each_other() {
let clear_result = Cli::try_parse_from(["wayscriber", "--clear-tool-state", "--clear-session"]);
Expand Down
24 changes: 24 additions & 0 deletions src/session/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ pub fn rename_session_display_name_by_id(
})
}

/// Rename a catalog entry's display name by session path. Session files are untouched.
#[allow(dead_code)]
pub fn rename_session_display_name_by_path(
path: &Path,
display_name: &str,
) -> Result<Option<CatalogEntry>> {
let display_name = display_name.trim();
if display_name.is_empty() {
return Err(anyhow!("session display name cannot be empty"));
}
let identity = session_path_identity(path);
with_catalog_write(|catalog| {
let Some(entry) = catalog
.sessions
.iter_mut()
.find(|entry| entry_matches_identity(entry, &identity))
else {
return Ok(None);
};
entry.display_name = display_name.to_string();
Ok(Some(entry.clone()))
})
}

/// Update a catalog entry's session path after a committed disk move.
#[allow(dead_code)]
pub fn move_session_path_by_id(id: &str, target_path: &Path) -> Result<Option<CatalogEntry>> {
Expand Down
Loading
Loading