Skip to content

Commit 8bdbac9

Browse files
sync from internal repository
1 parent 73a0a2f commit 8bdbac9

File tree

12 files changed

+224
-49
lines changed

12 files changed

+224
-49
lines changed

.github/workflows/close-prs.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Close PRs
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened]
6+
7+
jobs:
8+
close:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
pull-requests: write
12+
steps:
13+
- uses: actions/github-script@v7
14+
with:
15+
script: |
16+
await github.rest.issues.createComment({
17+
owner: context.repo.owner,
18+
repo: context.repo.repo,
19+
issue_number: context.payload.pull_request.number,
20+
body: 'we don\'t do that here'
21+
});
22+
await github.rest.pulls.update({
23+
owner: context.repo.owner,
24+
repo: context.repo.repo,
25+
pull_number: context.payload.pull_request.number,
26+
state: 'closed'
27+
});

README.md

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,76 @@
1-
# dkdc-links
1+
# Bookmarks CLI
22

3-
***Bookmarks in your terminal.***
3+
Bookmarks in your terminal.
4+
5+
## Install
6+
7+
Recommended:
8+
9+
```bash
10+
curl -LsSf https://dkdc.sh/dkdc-links/install.sh | sh
11+
```
12+
13+
uv:
14+
15+
```bash
16+
uv tool install dkdc-links
17+
```
18+
19+
cargo:
20+
21+
```bash
22+
cargo install dkdc-links
23+
```
24+
25+
You can use `uvx` to run it without installing:
26+
27+
```bash
28+
uvx dkdc-links
29+
```
30+
31+
## Usage
32+
33+
```
34+
dkdc-links [OPTIONS] [LINKS]...
35+
```
36+
37+
Open links by name or alias:
38+
39+
```bash
40+
# Open a link
41+
dkdc-links link1
42+
43+
# Open multiple links
44+
dkdc-links link1 link2
45+
46+
# Open a group
47+
dkdc-links dev
48+
```
49+
50+
### Options
51+
52+
| Flag | Short | Description |
53+
|------|-------|-------------|
54+
| `--config` | `-c` | Open config file in editor |
55+
| `--gui` | | Open graphical interface (under construction) |
56+
| `--help` | `-h` | Print help |
57+
| `--version` | `-V` | Print version |
58+
59+
## Configuration
60+
61+
Config lives at `~/.config/dkdc/links/config.toml`. Run `dkdc-links -c` to edit it.
62+
63+
```toml
64+
[aliases]
65+
gh = "github"
66+
li = "linkedin"
67+
68+
[links]
69+
github = "https://github.com/lostmygithubaccount"
70+
linkedin = "https://linkedin.com/in/codydkdc"
71+
72+
[groups]
73+
social = ["github", "linkedin"]
74+
```
75+
76+
Aliases map to links, links map to URLs, and groups expand to multiple aliases or links.

bin/setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ set -euo pipefail
55

66
if ! command -v rustup &>/dev/null; then
77
echo "Installing rustup..."
8-
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
8+
curl --proto '=https' --tlsv1.2 -LsSf https://sh.rustup.rs | sh -s -- -y
99
. "$HOME/.cargo/env"
1010
fi
1111

dkdc-links-py/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dkdc-links-py/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "dkdc-links-py"
5-
version = "2.0.1"
5+
version = "2.1.0"
66
edition = "2024"
77
authors = ["Cody <cody@dkdc.dev>"]
88
license = "MIT"

dkdc-links/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dkdc-links/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "dkdc-links"
5-
version = "2.0.1"
5+
version = "2.1.0"
66
edition = "2024"
77
authors = ["Cody <cody@dkdc.dev>"]
88
description = "Bookmarks in your terminal"

dkdc-links/src/config.rs

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Context, Result};
22
use serde::{Deserialize, Serialize};
3+
use std::borrow::Cow;
34
use std::collections::HashMap;
45
use std::fs;
56
use std::path::PathBuf;
@@ -33,7 +34,36 @@ link2 = "https://github.com/lostmygithubaccount/dkdc-links"
3334
dev = ["alias1", "alias2"]
3435
"#;
3536

37+
impl Config {
38+
pub fn validate(&self) -> Vec<String> {
39+
let mut warnings = Vec::new();
40+
41+
for (alias, target) in &self.aliases {
42+
if !self.links.contains_key(target) {
43+
warnings.push(format!(
44+
"alias '{alias}' points to '{target}' which is not in [links]"
45+
));
46+
}
47+
}
48+
49+
for (group, entries) in &self.groups {
50+
for entry in entries {
51+
if !self.aliases.contains_key(entry) && !self.links.contains_key(entry) {
52+
warnings.push(format!(
53+
"group '{group}' contains '{entry}' which is not in [aliases] or [links]"
54+
));
55+
}
56+
}
57+
}
58+
59+
warnings
60+
}
61+
}
62+
3663
pub fn config_path() -> Result<PathBuf> {
64+
// Intentionally use ~/.config/ rather than dirs::config_dir(), which
65+
// returns ~/Library/Application Support/ on macOS. We want a single
66+
// consistent dotfile location across platforms.
3767
let home = dirs::home_dir().context("Failed to get home directory")?;
3868
Ok(home
3969
.join(CONFIG_DIR)
@@ -46,7 +76,9 @@ pub fn init_config() -> Result<()> {
4676
let config_path = config_path()?;
4777

4878
if !config_path.exists() {
49-
let config_dir = config_path.parent().unwrap();
79+
let config_dir = config_path
80+
.parent()
81+
.context("Invalid config path: no parent directory")?;
5082
fs::create_dir_all(config_dir).context("Failed to create config directory")?;
5183
fs::write(&config_path, DEFAULT_CONFIG).context("Failed to write default config")?;
5284
}
@@ -58,6 +90,11 @@ pub fn load_config() -> Result<Config> {
5890
let config_path = config_path()?;
5991
let contents = fs::read_to_string(&config_path).context("Failed to read config file")?;
6092
let config: Config = toml::from_str(&contents).context("Failed to parse config file")?;
93+
94+
for warning in config.validate() {
95+
eprintln!("[dkdc-links] warning: {warning}");
96+
}
97+
6198
Ok(config)
6299
}
63100

@@ -79,35 +116,36 @@ pub fn edit_config() -> Result<()> {
79116
Ok(())
80117
}
81118

82-
fn print_section<V, F>(name: &str, map: &HashMap<String, V>, format_value: F)
83-
where
84-
F: Fn(&V) -> String,
85-
{
119+
fn print_section<V>(
120+
name: &str,
121+
map: &HashMap<String, V>,
122+
format_value: impl Fn(&V) -> Cow<'_, str>,
123+
) {
86124
if map.is_empty() {
87125
return;
88126
}
89127

90128
println!("{name}:");
91129
println!();
92130

93-
let mut keys: Vec<_> = map.keys().collect();
94-
keys.sort_unstable();
131+
let mut entries: Vec<_> = map.iter().collect();
132+
entries.sort_unstable_by_key(|(k, _)| k.as_str());
95133

96-
let max_key_len = keys.iter().map(|k| k.len()).max().unwrap_or(0);
134+
let max_key_len = entries.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
97135

98-
for key in keys {
99-
if let Some(value) = map.get(key) {
100-
println!("• {key:<max_key_len$} | {}", format_value(value));
101-
}
136+
for (key, value) in entries {
137+
println!("• {key:<max_key_len$} | {}", format_value(value));
102138
}
103139

104140
println!();
105141
}
106142

107143
pub fn print_config(config: &Config) {
108-
print_section("aliases", &config.aliases, |v| v.clone());
109-
print_section("links", &config.links, |v| v.clone());
110-
print_section("groups", &config.groups, |v| format!("[{}]", v.join(", ")));
144+
print_section("aliases", &config.aliases, |v| Cow::Borrowed(v));
145+
print_section("links", &config.links, |v| Cow::Borrowed(v));
146+
print_section("groups", &config.groups, |v| {
147+
Cow::Owned(format!("[{}]", v.join(", ")))
148+
});
111149
}
112150

113151
#[cfg(test)]
@@ -183,4 +221,40 @@ rust = "https://rust-lang.org"
183221
assert!(!config.links.is_empty());
184222
assert!(!config.groups.is_empty());
185223
}
224+
225+
#[test]
226+
fn test_valid_config_has_no_warnings() {
227+
let config: Config = toml::from_str(DEFAULT_CONFIG).unwrap();
228+
assert!(config.validate().is_empty());
229+
}
230+
231+
#[test]
232+
fn test_broken_alias_target_warns() {
233+
let toml = r#"
234+
[aliases]
235+
broken = "nonexistent"
236+
237+
[links]
238+
real = "https://example.com"
239+
"#;
240+
let config: Config = toml::from_str(toml).unwrap();
241+
let warnings = config.validate();
242+
assert_eq!(warnings.len(), 1);
243+
assert!(warnings[0].contains("nonexistent"));
244+
}
245+
246+
#[test]
247+
fn test_broken_group_entry_warns() {
248+
let toml = r#"
249+
[links]
250+
real = "https://example.com"
251+
252+
[groups]
253+
dev = ["real", "ghost"]
254+
"#;
255+
let config: Config = toml::from_str(toml).unwrap();
256+
let warnings = config.validate();
257+
assert_eq!(warnings.len(), 1);
258+
assert!(warnings[0].contains("ghost"));
259+
}
186260
}

dkdc-links/src/gui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod colors {
1313

1414
struct Links;
1515

16-
#[derive(Debug, Clone)]
16+
#[derive(Debug, Clone, Copy)]
1717
enum Message {}
1818

1919
impl Links {

0 commit comments

Comments
 (0)