provozni zaloha

This commit is contained in:
lachtan
2026-06-24 08:11:12 +02:00
parent 9295dba19f
commit 1db3ec4756
97 changed files with 7698 additions and 817 deletions

View File

@@ -35,29 +35,40 @@ bookmark.py add "https://example.com/rust-async" "Async Rust patterns" --tags ru
bookmark.py list [--tag <tag>]
```
Shows ID, URL, tags, description, and date added for each unread bookmark. Use `--tag` to filter.
Shows display ID, URL, tags, description, and date added for each unread bookmark. Use `--tag` to filter (display IDs stay global, so a filtered list may show gaps).
### Display IDs
The `#1`, `#2`, … shown by `list` and `history` are **display IDs** — sequential positions, computed on the fly, never the internal DB id. They renumber whenever the set changes, so run `list`/`history` first if unsure.
- `read <n>` and `show <n>` take the display ID from **`list`** (the unread set).
- `unread <n>` takes the display ID from **`history`** (the read set).
A freshly added bookmark is always display `#1` in `list` (newest first).
### Mark as read
```bash
bookmark.py read <id>
bookmark.py read <display-id>
```
Marks bookmark as read (stores `read_at` timestamp). Does **not** delete — entry stays in DB.
`<display-id>` is the number from `list`. Marks bookmark as read (stores `read_at` timestamp). Does **not** delete — entry stays in DB.
### Unmark (mark as unread again)
```bash
bookmark.py unread <id>
bookmark.py unread <display-id>
```
`<display-id>` is the number from `history`.
### Show bookmark details
```bash
bookmark.py show <id>
bookmark.py show <display-id>
```
Shows full URL, description, tags, status (read/unread), and dates. Does **not** change any state.
`<display-id>` is the number from `list`. Shows full URL, description, tags, status, and dates. Does **not** change any state.
### List read bookmarks (history)
@@ -65,7 +76,7 @@ Shows full URL, description, tags, status (read/unread), and dates. Does **not**
bookmark.py history
```
Shows all bookmarks marked as read, with both `added` and `read` dates.
Shows all bookmarks marked as read, with both `added` and `read` dates, numbered with their own display IDs.
## Output formatting
@@ -75,7 +86,7 @@ When presenting bookmark lists or details to the user, **always use markdown lin
#3 [hackaday.com](https://hackaday.com/2026/06/02/linux-fu-taming-strace/) [linux, strace] — lepší strace
```
Format: `#<id> [<domain>](<url>) [<tags>] — <description>`
Format: `#<display-id> [<domain>](<url>) [<tags>] — <description>`
- Domain is clickable, pointing to the full URL
- Tags in brackets, comma-separated
@@ -86,6 +97,6 @@ Format: `#<id> [<domain>](<url>) [<tags>] — <description>`
1. User shares a URL → `add` with description and optional tags
2. User wants to see what to read → `list`
3. User wants to see details of a bookmark → `show <id>`
4. User finishes an article → `read <id>`
5. User wants to revisit → `unread <id>` or `history`
3. User wants to see details of a bookmark → `show <display-id>` (from `list`)
4. User finishes an article → `read <display-id>` (from `list`)
5. User wants to revisit → `unread <display-id>` (from `history`) or `history`

View File

@@ -44,6 +44,33 @@ def _connect() -> sqlite3.Connection:
conn.close()
def _ordered_ids(conn: sqlite3.Connection, *, read: bool) -> list[int]:
"""Internal ids of one bookmark set in display order.
Unread (`read=False`) is what `list` shows, read (`read=True`) what `history`
shows. Display IDs are 1-based positions here, computed on the fly — never
stored — so they renumber whenever the set changes.
"""
if read:
rows = conn.execute(
"SELECT id FROM bookmarks WHERE read_at IS NOT NULL ORDER BY read_at DESC"
).fetchall()
else:
rows = conn.execute(
"SELECT id FROM bookmarks WHERE read_at IS NULL ORDER BY created_at DESC"
).fetchall()
return [row["id"] for row in rows]
def _resolve_display_id(conn: sqlite3.Connection, display_id: int, *, read: bool) -> int | None:
"""Translate a display ID into an internal id, or None if out of range."""
order = _ordered_ids(conn, read=read)
idx = display_id - 1
if idx < 0 or idx >= len(order):
return None
return order[idx]
def _parse_tags(raw: str) -> list[str]:
"""Parse comma-separated tags into a deduplicated sorted list."""
if not raw:
@@ -68,12 +95,12 @@ def _domain(url: str) -> str:
def _print_bookmark(
row: sqlite3.Row, *, show_status: bool = False, show_read_date: bool = False
row: sqlite3.Row, display_id: int, *, show_status: bool = False, show_read_date: bool = False
) -> None:
"""Format and print a single bookmark row."""
"""Format and print a single bookmark row under its display ID."""
tags = json.loads(row["tags"])
tag_str = f" [{', '.join(tags)}]" if tags else ""
print(f"#{row['id']} {_domain(row['url'])}{tag_str}")
print(f"#{display_id} {_domain(row['url'])}{tag_str}")
print(f" {row['description']}")
print(f" {row['url']}")
line = f" added: {row['created_at'][:10]}"
@@ -94,13 +121,14 @@ def cmd_add(args: argparse.Namespace) -> None:
(args.url, args.description, json.dumps(tags, ensure_ascii=False), now),
)
conn.commit()
bid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
tag_info = f" [{', '.join(tags)}]" if tags else ""
print(f"Added bookmark #{bid}: {args.url}{tag_info}")
# Newest unread sorts first, so a fresh bookmark is always display #1.
print(f"Added bookmark #1: {args.url}{tag_info}")
def cmd_list(args: argparse.Namespace) -> None:
with _connect() as conn:
display_by_id = {nid: i + 1 for i, nid in enumerate(_ordered_ids(conn, read=False))}
if args.tag:
rows = conn.execute(
"""SELECT * FROM bookmarks
@@ -121,49 +149,44 @@ def cmd_list(args: argparse.Namespace) -> None:
)
return
# Display IDs come from the full unread set so a tag-filtered list keeps the
# same numbers `read`/`show` resolve against (gaps are expected when filtered).
for r in rows:
_print_bookmark(r)
_print_bookmark(r, display_by_id[r["id"]])
print()
def cmd_read(args: argparse.Namespace) -> None:
with _connect() as conn:
internal_id = _resolve_display_id(conn, args.id, read=False)
if internal_id is None:
print(f"No unread bookmark #{args.id}.")
return
now = datetime.now(timezone.utc).isoformat()
cur = conn.execute(
"UPDATE bookmarks SET read_at = ? WHERE id = ? AND read_at IS NULL",
(now, args.id),
)
affected = cur.rowcount
conn.execute("UPDATE bookmarks SET read_at = ? WHERE id = ?", (now, internal_id))
conn.commit()
if affected == 0:
print(f"Bookmark #{args.id} not found or already marked as read.")
else:
print(f"Marked bookmark #{args.id} as read.")
print(f"Marked bookmark #{args.id} as read.")
def cmd_unread(args: argparse.Namespace) -> None:
with _connect() as conn:
cur = conn.execute(
"UPDATE bookmarks SET read_at = NULL WHERE id = ? AND read_at IS NOT NULL",
(args.id,),
)
affected = cur.rowcount
internal_id = _resolve_display_id(conn, args.id, read=True)
if internal_id is None:
print(f"No read bookmark #{args.id} in history.")
return
conn.execute("UPDATE bookmarks SET read_at = NULL WHERE id = ?", (internal_id,))
conn.commit()
if affected == 0:
print(f"Bookmark #{args.id} not found or not marked as read.")
else:
print(f"Unmarked bookmark #{args.id}.")
print(f"Unmarked bookmark #{args.id}.")
def cmd_show(args: argparse.Namespace) -> None:
with _connect() as conn:
row = conn.execute(
"SELECT * FROM bookmarks WHERE id = ?", (args.id,)
).fetchone()
if not row:
print(f"Bookmark #{args.id} not found.")
return
_print_bookmark(row, show_status=True)
internal_id = _resolve_display_id(conn, args.id, read=False)
if internal_id is None:
print(f"No unread bookmark #{args.id}.")
return
row = conn.execute("SELECT * FROM bookmarks WHERE id = ?", (internal_id,)).fetchone()
_print_bookmark(row, args.id, show_status=True)
def cmd_history(args: argparse.Namespace) -> None:
@@ -176,8 +199,8 @@ def cmd_history(args: argparse.Namespace) -> None:
print("No read bookmarks.")
return
for r in rows:
_print_bookmark(r, show_read_date=True)
for display_id, r in enumerate(rows, start=1):
_print_bookmark(r, display_id, show_read_date=True)
print()
@@ -197,15 +220,15 @@ def main() -> None:
# read (mark as read)
p_read = sub.add_parser("read", help="Mark bookmark as read")
p_read.add_argument("id", type=int, help="Bookmark ID")
p_read.add_argument("id", type=int, help="Display ID from `list`")
# unread (unmark)
p_unread = sub.add_parser("unread", help="Unmark bookmark as read")
p_unread.add_argument("id", type=int, help="Bookmark ID")
p_unread.add_argument("id", type=int, help="Display ID from `history`")
# show (display details)
p_show = sub.add_parser("show", help="Show bookmark details")
p_show.add_argument("id", type=int, help="Bookmark ID")
p_show.add_argument("id", type=int, help="Display ID from `list`")
# history (list read)
sub.add_parser("history", help="List read bookmarks")