feat: copy the output panel to the system clipboard (#11)
New app-level `copy` / `copy all` / `copy last` command (ADR-0041). Delivery is OSC 52 *and* a best-effort native write (arboard), always both — OSC 52 acceptance is undetectable, so a true fallback can't be built. Payload is the panel's plain text exactly as rendered (tags, ✓/✗, box-drawing), drift-locked to render_output_line. arboard added --no-default-features (X11-only; OSC 52 covers Wayland). Amends ADR-0003's command registry; requirements V6.
This commit is contained in:
@@ -516,6 +516,19 @@ pub enum AppCommand {
|
||||
Undo,
|
||||
/// Re-apply the most recently undone change, after confirmation.
|
||||
Redo,
|
||||
/// Copy the output panel to the system clipboard (ADR-0041).
|
||||
/// `copy` / `copy all` copy the whole panel; `copy last` copies
|
||||
/// the most recent command's output.
|
||||
Copy { scope: CopyScope },
|
||||
}
|
||||
|
||||
/// Which slice of the output panel `copy` targets (ADR-0041).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CopyScope {
|
||||
/// The entire output buffer (`copy` bare, or `copy all`).
|
||||
All,
|
||||
/// From the most recent echo line to the end (`copy last`).
|
||||
Last,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -871,6 +884,7 @@ impl Command {
|
||||
AppCommand::Messages { .. } => "messages",
|
||||
AppCommand::Undo => "undo",
|
||||
AppCommand::Redo => "redo",
|
||||
AppCommand::Copy { .. } => "copy",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+50
-1
@@ -7,7 +7,7 @@
|
||||
//! builder, help / usage references. The ast_builders match
|
||||
//! against the `MatchedPath` items in declaration order.
|
||||
|
||||
use crate::dsl::command::{AppCommand, Command, MessagesValue, ModeValue};
|
||||
use crate::dsl::command::{AppCommand, Command, CopyScope, MessagesValue, ModeValue};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError,
|
||||
Word,
|
||||
@@ -37,8 +37,16 @@ fn validate_unknown_messages(value: &str) -> Result<(), ValidationError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_unknown_copy(value: &str) -> Result<(), ValidationError> {
|
||||
Err(ValidationError {
|
||||
message_key: "copy.unknown",
|
||||
args: vec![("value", value.to_string())],
|
||||
})
|
||||
}
|
||||
|
||||
const UNKNOWN_MODE_VALIDATOR: IdentValidator = validate_unknown_mode;
|
||||
const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages;
|
||||
const UNKNOWN_COPY_VALIDATOR: IdentValidator = validate_unknown_copy;
|
||||
|
||||
// --- Shapes (constants are referenced by Optional/Choice slices) --
|
||||
|
||||
@@ -114,6 +122,29 @@ const MESSAGES_CHOICES: &[Node] = &[
|
||||
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
||||
const MESSAGES_VALUE_OPT: Node = Node::Optional(&MESSAGES_VALUE);
|
||||
|
||||
// `copy [all|last]`: same shape as `messages` — known scope words are
|
||||
// `Word` siblings (so they reach completion + the expected set); the
|
||||
// trailing catch-all `Ident` funnels any other word into the friendly
|
||||
// `copy.unknown` validator. Bare `copy` (no value) means `all`.
|
||||
const COPY_CHOICES: &[Node] = &[
|
||||
Node::Word(Word::keyword("all")),
|
||||
Node::Word(Word::keyword("last")),
|
||||
Node::Ident {
|
||||
source: IdentSource::Free,
|
||||
role: "copy_value",
|
||||
validator: Some(UNKNOWN_COPY_VALIDATOR),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
},
|
||||
];
|
||||
const COPY_VALUE: Node = Node::Choice(COPY_CHOICES);
|
||||
const COPY_VALUE_OPT: Node = Node::Optional(©_VALUE);
|
||||
|
||||
const EMPTY_SEQ: Node = Node::Seq(&[]);
|
||||
const SAVE_AS_OPT: Node = Node::Optional(&SAVE_AS_WORD);
|
||||
|
||||
@@ -202,6 +233,17 @@ fn build_messages(path: &MatchedPath, _source: &str) -> Result<Command, Validati
|
||||
Ok(Command::App(AppCommand::Messages { value }))
|
||||
}
|
||||
|
||||
fn build_copy(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
// The unknown-value branch's validator always errors, so reaching
|
||||
// here means either a known scope word or a bare `copy` (= all).
|
||||
let scope = if path.contains_word("last") {
|
||||
CopyScope::Last
|
||||
} else {
|
||||
CopyScope::All
|
||||
};
|
||||
Ok(Command::App(AppCommand::Copy { scope }))
|
||||
}
|
||||
|
||||
// --- Command nodes -------------------------------------------------
|
||||
|
||||
pub static QUIT: CommandNode = CommandNode {
|
||||
@@ -287,3 +329,10 @@ pub static REDO: CommandNode = CommandNode {
|
||||
ast_builder: build_redo,
|
||||
help_id: Some("app.redo"),
|
||||
usage_ids: &["parse.usage.redo"],};
|
||||
|
||||
pub static COPY: CommandNode = CommandNode {
|
||||
entry: Word::keyword("copy"),
|
||||
shape: COPY_VALUE_OPT,
|
||||
ast_builder: build_copy,
|
||||
help_id: Some("app.copy"),
|
||||
usage_ids: &["parse.usage.copy"],};
|
||||
|
||||
@@ -613,6 +613,7 @@ pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
|
||||
(&app::MESSAGES, CommandCategory::Simple),
|
||||
(&app::UNDO, CommandCategory::Simple),
|
||||
(&app::REDO, CommandCategory::Simple),
|
||||
(&app::COPY, CommandCategory::Simple),
|
||||
(&ddl::DROP, CommandCategory::Simple),
|
||||
(&ddl::ADD, CommandCategory::Simple),
|
||||
(&ddl::RENAME, CommandCategory::Simple),
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ pub mod walker;
|
||||
|
||||
pub use action::ReferentialAction;
|
||||
pub use command::{
|
||||
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, Expr,
|
||||
AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr,
|
||||
IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter,
|
||||
SqlForeignKey,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user