v0.0.6 - TUI: add Edit role command, Up/Down role selection for Edit/Delete, no highlight on Add
This commit is contained in:
Generated
+1
-1
@@ -1490,7 +1490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nsigner"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chacha20poly1305",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nsigner"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Attended Nostr signing daemon — Rust port of n_signer"
|
||||
|
||||
+1
-1
@@ -31,4 +31,4 @@ pub mod error;
|
||||
pub use error::NsignerError;
|
||||
|
||||
/// Version string (matches C NSIGNER_VERSION).
|
||||
pub const VERSION: &str = "v0.0.5";
|
||||
pub const VERSION: &str = "v0.0.6";
|
||||
|
||||
+161
-58
@@ -39,8 +39,8 @@ pub const TRANSPORT_HTTP: u8 = 0x08;
|
||||
pub const QREXEC_SERVICE_NAME: &str = "qubes.NsignerRpc";
|
||||
|
||||
/// Number of focusable commands on the main screen:
|
||||
/// 4 transports + Add + Delete + Clear + Help + Quit.
|
||||
pub const FOCUS_COUNT: usize = 9;
|
||||
/// 4 transports + Add + Edit + Delete + Clear + Help + Quit.
|
||||
pub const FOCUS_COUNT: usize = 10;
|
||||
|
||||
/// Look up the local FIPS node identity by running `fipsctl show status`.
|
||||
/// Returns the node's `npub` (a Nostr public-key identifier) when both
|
||||
@@ -387,6 +387,10 @@ pub struct App {
|
||||
pub role_add_default_purpose: RolePurpose,
|
||||
/// Saved OTP pad directory between the OtpDir and OtpName stages.
|
||||
pub role_add_otp_dir: String,
|
||||
/// If true, the add-role popup is editing an existing role instead of
|
||||
/// creating a new one. The original name is stored for lookup.
|
||||
pub role_edit_mode: bool,
|
||||
pub role_edit_original_name: String,
|
||||
|
||||
// Transport
|
||||
pub transport_toggles: [bool; 4],
|
||||
@@ -399,8 +403,8 @@ pub struct App {
|
||||
|
||||
// Unified command focus.
|
||||
/// Index of the currently focused command on the main screen.
|
||||
/// 0-3 = transports (Unix, Qube, FIPS, HTTP), 4 = Add, 5 = Delete,
|
||||
/// 6 = Clear, 7 = Help, 8 = Quit. Tab cycles forward.
|
||||
/// 0-3 = transports (Unix, Qube, FIPS, HTTP), 4 = Add, 5 = Edit,
|
||||
/// 6 = Delete, 7 = Clear, 8 = Help, 9 = Quit. Tab cycles forward.
|
||||
pub focus: usize,
|
||||
|
||||
// Main screen
|
||||
@@ -448,6 +452,8 @@ impl App {
|
||||
role_add_default_curve: "secp256k1".to_string(),
|
||||
role_add_default_purpose: RolePurpose::Nostr,
|
||||
role_add_otp_dir: String::new(),
|
||||
role_edit_mode: false,
|
||||
role_edit_original_name: String::new(),
|
||||
transport_toggles: [true, false, false, false],
|
||||
transport_mask: TRANSPORT_UNIX,
|
||||
transport_cursor: 0,
|
||||
@@ -663,10 +669,15 @@ impl App {
|
||||
self.should_quit = true;
|
||||
}
|
||||
KeyCode::Char('a') | KeyCode::Char('A') => {
|
||||
self.role_edit_mode = false;
|
||||
self.role_edit_original_name.clear();
|
||||
self.role_add_stage = RoleAddStage::PresetMenu;
|
||||
self.role_add_choice = 1;
|
||||
self.screen = Screen::AddRole;
|
||||
}
|
||||
KeyCode::Char('e') | KeyCode::Char('E') => {
|
||||
self.edit_selected_role();
|
||||
}
|
||||
KeyCode::Char('d') | KeyCode::Char('D') => {
|
||||
self.delete_selected_role();
|
||||
}
|
||||
@@ -698,14 +709,28 @@ impl App {
|
||||
}
|
||||
// Enter activates whichever command is currently focused.
|
||||
KeyCode::Enter => self.activate_focused(),
|
||||
// Up/Down and j/k scroll the activity log.
|
||||
// Up/Down and j/k move role selection when a role command
|
||||
// (Edit / Delete) is focused, otherwise scroll the log.
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if self.activity_scroll > 0 {
|
||||
if self.focus == 5 || self.focus == 6 {
|
||||
// Move role cursor up
|
||||
if self.role_cursor > 0 {
|
||||
self.role_cursor -= 1;
|
||||
}
|
||||
} else if self.activity_scroll > 0 {
|
||||
self.activity_scroll -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.activity_scroll += 1;
|
||||
if self.focus == 5 || self.focus == 6 {
|
||||
// Move role cursor down
|
||||
let last = self.role_table.entries.len().saturating_sub(1);
|
||||
if self.role_cursor < last {
|
||||
self.role_cursor += 1;
|
||||
}
|
||||
} else {
|
||||
self.activity_scroll += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -719,17 +744,20 @@ impl App {
|
||||
2 => self.toggle_transport(2),
|
||||
3 => self.toggle_transport(3),
|
||||
4 => {
|
||||
self.role_edit_mode = false;
|
||||
self.role_edit_original_name.clear();
|
||||
self.role_add_stage = RoleAddStage::PresetMenu;
|
||||
self.role_add_choice = 1;
|
||||
self.screen = Screen::AddRole;
|
||||
}
|
||||
5 => self.delete_selected_role(),
|
||||
6 => {
|
||||
5 => self.edit_selected_role(),
|
||||
6 => self.delete_selected_role(),
|
||||
7 => {
|
||||
self.activity_log.clear();
|
||||
self.activity_scroll = 0;
|
||||
}
|
||||
7 => self.screen = Screen::Help,
|
||||
8 => self.should_quit = true,
|
||||
8 => self.screen = Screen::Help,
|
||||
9 => self.should_quit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -778,10 +806,34 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the add-role popup pre-filled with the selected role's data
|
||||
/// for editing. Sets `role_edit_mode` so that on confirm the old
|
||||
/// entry is replaced rather than a new one being added.
|
||||
fn edit_selected_role(&mut self) {
|
||||
let count = self.role_table.entries.len();
|
||||
if count == 0 || self.role_cursor >= count {
|
||||
return;
|
||||
}
|
||||
let entry = &self.role_table.entries[self.role_cursor];
|
||||
self.role_edit_mode = true;
|
||||
self.role_edit_original_name = entry.name.clone();
|
||||
self.role_add_default_name = entry.name.clone();
|
||||
self.role_add_default_path = entry.role_path.clone();
|
||||
self.role_add_default_curve = entry.curve_str.clone();
|
||||
self.role_add_default_purpose = entry.purpose;
|
||||
self.role_add_choice = 10; // custom — skip preset, go to NameEntry
|
||||
self.role_add_input = InputField::new(&entry.name);
|
||||
self.role_add_stage = RoleAddStage::NameEntry;
|
||||
self.role_add_name = entry.name.clone();
|
||||
self.screen = Screen::AddRole;
|
||||
}
|
||||
|
||||
// ── Add-role popup ──────────────────────────────────────────────────
|
||||
|
||||
fn handle_add_role_key(&mut self, key: KeyEvent) {
|
||||
if key.code == KeyCode::Esc {
|
||||
self.role_edit_mode = false;
|
||||
self.role_edit_original_name.clear();
|
||||
self.screen = Screen::Main;
|
||||
return;
|
||||
}
|
||||
@@ -985,25 +1037,41 @@ impl App {
|
||||
self.role_add_input = InputField::new("");
|
||||
return;
|
||||
}
|
||||
if self.role_table.find_by_name(&name).is_some() {
|
||||
// Duplicate — back to preset
|
||||
// In edit mode, the name may match the original — that's fine.
|
||||
// Only reject duplicates that belong to a *different* role.
|
||||
if !self.role_edit_mode {
|
||||
if self.role_table.find_by_name(&name).is_some() {
|
||||
// Duplicate — back to preset
|
||||
self.role_add_stage = RoleAddStage::PresetMenu;
|
||||
return;
|
||||
}
|
||||
} else if name != self.role_edit_original_name
|
||||
&& self.role_table.find_by_name(&name).is_some()
|
||||
{
|
||||
// Renamed to collide with another existing role.
|
||||
self.role_add_stage = RoleAddStage::PresetMenu;
|
||||
return;
|
||||
}
|
||||
self.role_add_name = name.clone();
|
||||
match self.role_add_choice {
|
||||
// Custom: pick the curve next, then edit the path.
|
||||
10 => {
|
||||
self.role_add_stage = RoleAddStage::CurveSelect;
|
||||
}
|
||||
// OTP: collect the pad directory and pad name next.
|
||||
9 => {
|
||||
self.role_add_input = InputField::new("");
|
||||
self.role_add_stage = RoleAddStage::OtpDir;
|
||||
}
|
||||
// Presets with a fixed path: register immediately.
|
||||
_ => {
|
||||
self.add_role_register_path(&name);
|
||||
if self.role_edit_mode {
|
||||
// Editing: go straight to path entry (curve is already known).
|
||||
self.role_add_input = InputField::new(&self.role_add_default_path);
|
||||
self.role_add_stage = RoleAddStage::PathEntry;
|
||||
} else {
|
||||
match self.role_add_choice {
|
||||
// Custom: pick the curve next, then edit the path.
|
||||
10 => {
|
||||
self.role_add_stage = RoleAddStage::CurveSelect;
|
||||
}
|
||||
// OTP: collect the pad directory and pad name next.
|
||||
9 => {
|
||||
self.role_add_input = InputField::new("");
|
||||
self.role_add_stage = RoleAddStage::OtpDir;
|
||||
}
|
||||
// Presets with a fixed path: register immediately.
|
||||
_ => {
|
||||
self.add_role_register_path(&name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1032,32 +1100,57 @@ impl App {
|
||||
let purpose = self.role_add_default_purpose;
|
||||
match crate::role_table::parse_path_template(path) {
|
||||
Ok((template, range_lo, range_hi, allowed_indices)) => {
|
||||
match self.role_table.register_role_path(
|
||||
&name,
|
||||
&template,
|
||||
purpose,
|
||||
curve,
|
||||
range_lo,
|
||||
range_hi,
|
||||
-1,
|
||||
&allowed_indices,
|
||||
) {
|
||||
Ok(()) => {
|
||||
if self.role_edit_mode {
|
||||
// Replace the old entry in-place.
|
||||
let old_name = self.role_edit_original_name.clone();
|
||||
if let Some(pos) = self.role_table.entries.iter().position(|e| e.name == old_name)
|
||||
{
|
||||
self.role_table.entries[pos].name = name.clone();
|
||||
self.role_table.entries[pos].purpose = purpose;
|
||||
self.role_table.entries[pos].curve = curve;
|
||||
self.role_table.entries[pos].role_path = template.clone();
|
||||
self.role_table.entries[pos].path_range_lo = range_lo;
|
||||
self.role_table.entries[pos].path_range_hi = range_hi;
|
||||
self.role_table.entries[pos].path_allowed_indices = allowed_indices.clone();
|
||||
self.role_table.entries[pos].path_default_index = -1;
|
||||
// Re-derive this role's key.
|
||||
self.key_store.wipe();
|
||||
let _ = self.key_store.derive_all(&mut self.role_table, &self.mnemonic);
|
||||
self.activity_log
|
||||
.add(&format!("role '{}' registered", name));
|
||||
// Derive the new role's key immediately.
|
||||
if let Some(idx) =
|
||||
self.role_table.entries.iter().position(|e| e.name == name)
|
||||
{
|
||||
let _ = self
|
||||
.key_store
|
||||
.derive_one(&mut self.role_table, &self.mnemonic, idx);
|
||||
}
|
||||
self.role_cursor = self.role_table.entries.len().saturating_sub(1);
|
||||
// Return to Main immediately — no extra Enter needed.
|
||||
self.screen = Screen::Main;
|
||||
.add(&format!("role '{}' updated", name));
|
||||
self.role_cursor = pos;
|
||||
}
|
||||
self.role_edit_mode = false;
|
||||
self.role_edit_original_name.clear();
|
||||
self.screen = Screen::Main;
|
||||
} else {
|
||||
match self.role_table.register_role_path(
|
||||
&name,
|
||||
&template,
|
||||
purpose,
|
||||
curve,
|
||||
range_lo,
|
||||
range_hi,
|
||||
-1,
|
||||
&allowed_indices,
|
||||
) {
|
||||
Ok(()) => {
|
||||
self.activity_log
|
||||
.add(&format!("role '{}' registered", name));
|
||||
// Derive the new role's key immediately.
|
||||
if let Some(idx) =
|
||||
self.role_table.entries.iter().position(|e| e.name == name)
|
||||
{
|
||||
let _ = self
|
||||
.key_store
|
||||
.derive_one(&mut self.role_table, &self.mnemonic, idx);
|
||||
}
|
||||
self.role_cursor = self.role_table.entries.len().saturating_sub(1);
|
||||
// Return to Main immediately — no extra Enter needed.
|
||||
self.screen = Screen::Main;
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -1295,9 +1388,9 @@ impl App {
|
||||
self.draw_roles_section(f, roles);
|
||||
self.draw_activity_section(f, right);
|
||||
|
||||
// Bottom bar: Help (focus 7) / Quit (focus 8).
|
||||
let help_focused = self.focus == 7;
|
||||
let quit_focused = self.focus == 8;
|
||||
// Bottom bar: Help (focus 8) / Quit (focus 9).
|
||||
let help_focused = self.focus == 8;
|
||||
let quit_focused = self.focus == 9;
|
||||
let mut help_spans = App::cmd_hint('l', "Help");
|
||||
if help_focused {
|
||||
for s in &mut help_spans {
|
||||
@@ -1438,10 +1531,10 @@ impl App {
|
||||
)),
|
||||
];
|
||||
for (i, e) in self.role_table.entries.iter().enumerate() {
|
||||
// Only highlight a role row when a role command (Add/Delete)
|
||||
// Only highlight a role row when a role command (Edit/Delete)
|
||||
// is focused, so the table doesn't show a stale highlight by
|
||||
// default.
|
||||
let selected = (self.focus == 4 || self.focus == 5) && self.role_cursor == i;
|
||||
let selected = (self.focus == 5 || self.focus == 6) && self.role_cursor == i;
|
||||
let row = Line::from(format!(
|
||||
" {:<14} {:<8} {:<12} {}",
|
||||
e.name, e.purpose_str, e.curve_str, e.display_path()
|
||||
@@ -1455,9 +1548,10 @@ impl App {
|
||||
}
|
||||
f.render_widget(Paragraph::new(lines), rows_area);
|
||||
|
||||
// Add (focus 4) / Delete (focus 5) — reverse-highlight when focused.
|
||||
// Add (focus 4) / Edit (focus 5) / Delete (focus 6) — reverse-highlight when focused.
|
||||
let add_focused = self.focus == 4;
|
||||
let del_focused = self.focus == 5;
|
||||
let edit_focused = self.focus == 5;
|
||||
let del_focused = self.focus == 6;
|
||||
let mut spans = Vec::new();
|
||||
let mut add_spans = App::cmd_hint('a', "Add");
|
||||
if add_focused {
|
||||
@@ -1467,6 +1561,14 @@ impl App {
|
||||
}
|
||||
spans.extend(add_spans);
|
||||
spans.push(Span::raw(" "));
|
||||
let mut edit_spans = App::cmd_hint('e', "Edit");
|
||||
if edit_focused {
|
||||
for s in &mut edit_spans {
|
||||
*s = s.clone().style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
}
|
||||
}
|
||||
spans.extend(edit_spans);
|
||||
spans.push(Span::raw(" "));
|
||||
let mut del_spans = App::cmd_hint('d', "Delete");
|
||||
if del_focused {
|
||||
for s in &mut del_spans {
|
||||
@@ -1512,7 +1614,7 @@ impl App {
|
||||
.end_symbol(Some("▼"));
|
||||
f.render_stateful_widget(sb, log_area, &mut scrollbar_state);
|
||||
|
||||
let clear_focused = self.focus == 6;
|
||||
let clear_focused = self.focus == 7;
|
||||
let mut spans = App::cmd_hint('c', "Clear");
|
||||
if clear_focused {
|
||||
for s in &mut spans {
|
||||
@@ -1671,6 +1773,7 @@ impl App {
|
||||
Line::from("F Toggle FIPS transport"),
|
||||
Line::from("H Toggle HTTP transport"),
|
||||
Line::from("A Add a role"),
|
||||
Line::from("E Edit the selected role"),
|
||||
Line::from("D Delete the selected role"),
|
||||
Line::from("C Clear the activity log"),
|
||||
Line::from("L Open the Help screen"),
|
||||
|
||||
Reference in New Issue
Block a user