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 Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "plinks"
version = "0.1.4"
version = "0.1.5"
edition = "2024"
description = "Project-local link manager with shell commands and a ratatui TUI"
license = "MIT"
Expand Down
5 changes: 4 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use clap::{ArgGroup, Args, Parser, Subcommand};
use clap::{ArgAction, ArgGroup, Args, Parser, Subcommand};

#[derive(Debug, Parser)]
#[command(
name = "plinks",
version,
about = "Manage shared project links from the command line or TUI",
long_about = "Manage shared project links stored in project-links.toml.\n\nUse plinks to save docs, dashboards, tickets, and runbooks under stable project-local names so the whole repository can share them.",
after_help = "Examples:\n plinks add docs https://docs.rs --alias api --tag rust\n plinks open docs\n plinks open api\n plinks open --tag rust\n plinks list --tag rust\n plinks manage\n\nUse `plinks help <command>` for command-specific examples."
)]
pub struct Cli {
#[arg(short = 'v', action = ArgAction::Version, help = "Print version")]
pub version_flag: Option<bool>,
#[command(subcommand)]
pub command: Option<Command>,
}
Expand Down
105 changes: 104 additions & 1 deletion src/tui/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use super::editor::EditorState;
use super::state::{App, Mode};
Expand All @@ -15,6 +15,10 @@ pub enum EventResult {
}

pub fn handle_key(app: &mut App, key: KeyEvent) -> Result<EventResult> {
if matches!(key.kind, KeyEventKind::Release) {
return Ok(EventResult::None);
}

match app.mode.clone() {
Mode::Normal => handle_normal_mode(app, key),
Mode::Filter => Ok(handle_filter_mode(app, key)),
Expand Down Expand Up @@ -156,3 +160,102 @@ fn handle_discard_confirm_mode(app: &mut App, key: KeyEvent) -> EventResult {
_ => EventResult::None,
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::{CandidateLink, Config};
use crate::tui::editor::Field;

fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::empty())
}

fn release_key(code: KeyCode) -> KeyEvent {
KeyEvent::new_with_kind(code, KeyModifiers::empty(), KeyEventKind::Release)
}

fn sample_app() -> App {
let mut config = Config::default();
config
.save_link(
None,
CandidateLink::new(
"docs".into(),
"https://docs.rs".into(),
vec!["api".into()],
vec!["rust".into()],
None,
)
.unwrap(),
)
.unwrap();
App::new(config)
}

#[test]
fn ignores_release_events_in_filter_mode() {
let mut app = sample_app();
app.mode = Mode::Filter;

assert_eq!(
handle_key(&mut app, key(KeyCode::Char('a'))).unwrap(),
EventResult::None
);
assert_eq!(app.filter, "a");

assert_eq!(
handle_key(&mut app, release_key(KeyCode::Char('a'))).unwrap(),
EventResult::None
);
assert_eq!(app.filter, "a");
}

#[test]
fn ignores_release_events_in_editor_mode() {
let mut app = sample_app();
app.begin_new();

assert_eq!(
handle_key(&mut app, key(KeyCode::Char('a'))).unwrap(),
EventResult::None
);
let Mode::Editor(editor) = &app.mode else {
panic!("expected editor mode");
};
assert_eq!(editor.primary, "a");

assert_eq!(
handle_key(&mut app, release_key(KeyCode::Char('a'))).unwrap(),
EventResult::None
);
let Mode::Editor(editor) = &app.mode else {
panic!("expected editor mode");
};
assert_eq!(editor.primary, "a");
}

#[test]
fn ignores_release_events_for_tab_navigation() {
let mut app = sample_app();
app.begin_new();

assert_eq!(
handle_key(&mut app, key(KeyCode::Tab)).unwrap(),
EventResult::None
);
let Mode::Editor(editor) = &app.mode else {
panic!("expected editor mode");
};
assert_eq!(editor.active_field, Field::Url);

assert_eq!(
handle_key(&mut app, release_key(KeyCode::Tab)).unwrap(),
EventResult::None
);
let Mode::Editor(editor) = &app.mode else {
panic!("expected editor mode");
};
assert_eq!(editor.active_field, Field::Url);
}
}
27 changes: 27 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ fn help_output(args: &[&str]) -> String {
String::from_utf8(output.stdout).unwrap()
}

fn command_output(args: &[&str]) -> std::process::Output {
ProcessCommand::new(env!("CARGO_BIN_EXE_plinks"))
.args(args)
.output()
.unwrap()
}

#[test]
fn add_creates_file_when_absent() {
let temp = TempDir::new().unwrap();
Expand Down Expand Up @@ -317,6 +324,8 @@ fn top_level_help_lists_command_summaries_and_examples() {
let help = help_output(&["-h"]);

assert!(help.contains("Manage shared project links from the command line or TUI"));
assert!(help.contains("-v"));
assert!(help.contains("--version"));
assert!(help.contains("open"));
assert!(help.contains("Open a saved link by primary name, alias, or tag"));
assert!(help.contains("list"));
Expand All @@ -336,6 +345,24 @@ fn no_args_defaults_to_manage_command() {
assert!(cli.command.is_none());
}

#[test]
fn version_flags_print_crate_version() {
for args in [&["--version"][..], &["-v"][..]] {
let output = command_output(args);
assert!(
output.status.success(),
"version command failed with status {}.\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout, format!("plinks {}\n", env!("CARGO_PKG_VERSION")));
assert!(output.stderr.is_empty());
}
}

#[test]
fn add_help_explains_primary_name_and_options() {
let help = help_output(&["add", "-h"]);
Expand Down
Loading