Files
William Casarin ad35547582 refactor: collapse client label settings; drop CLI/settings toggles
The "top vs bottom" client label setting was cluttering the UI and
codebase with toggles that added little value. This consolidates client
label handling into one option, removes unused CLI/settings knobs, and
makes NoteView’s API consistent and fluent. Result: fewer knobs, less
branching, and a clearer, more predictable UI.

Now client labels are only shown in one place: selected notes.

- Drop `--show-client` arg in notedeck and `--show-note-client=top|bottom`
  args in notedeck_columns

- Remove `NotedeckOptions::ShowClient` and related CLI parsing

- Delete `ShowSourceClientOption` enum, settings UI, and
  `SettingsAction::SetShowSourceClient`

- Collapse `NoteOptions::{ClientNameTop, ClientNameBottom}` into a single
  `NoteOptions::ClientName`

- Add `NoteOptions::{Framed, UnreadIndicator}`

- Move “framed” and unread indicator into flags (no more ad‑hoc bools)

- Add new NoteView builder methods: `.client_name()`, `.frame()`,
  `.unread_indicator()`, and `.selected_style()`

- CLI flags for showing client labels have been removed

- `ClientNameTop`/`ClientNameBottom` replaced with `ClientName`

- API using `framed` or `show_unread_indicator` booleans must now use
  the new flag setters

Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-03 16:16:15 -07:00

208 lines
7.7 KiB
Rust

use std::collections::BTreeSet;
use crate::timeline::TimelineKind;
use enostr::{Filter, Pubkey};
use oot_bitset::{bitset_clear, bitset_get, bitset_set};
use tracing::{debug, error, info};
#[repr(u16)]
pub enum ColumnsFlag {
SinceOptimize,
Textmode,
Scramble,
NoMedia,
}
pub struct ColumnsArgs {
pub columns: Vec<ArgColumn>,
flags: [u16; 2],
}
impl ColumnsArgs {
pub fn is_flag_set(&self, flag: ColumnsFlag) -> bool {
bitset_get(&self.flags, flag as u16)
}
pub fn set_flag(&mut self, flag: ColumnsFlag) {
bitset_set(&mut self.flags, flag as u16)
}
pub fn clear_flag(&mut self, flag: ColumnsFlag) {
bitset_clear(&mut self.flags, flag as u16)
}
pub fn parse(args: &[String], deck_author: Option<&Pubkey>) -> (Self, BTreeSet<String>) {
let mut unrecognized_args = BTreeSet::new();
let mut res = Self {
columns: vec![],
flags: [0; 2],
};
// flag defaults
res.set_flag(ColumnsFlag::SinceOptimize);
let mut i = 0;
let len = args.len();
while i < len {
let arg = &args[i];
if arg == "--textmode" {
res.set_flag(ColumnsFlag::Textmode);
} else if arg == "--no-since-optimize" {
res.clear_flag(ColumnsFlag::SinceOptimize);
} else if arg == "--scramble" {
res.set_flag(ColumnsFlag::Scramble);
} else if arg == "--no-media" {
res.set_flag(ColumnsFlag::NoMedia);
} else if arg == "--filter" {
i += 1;
let filter = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("filter argument missing?");
continue;
};
if let Ok(filter) = Filter::from_json(filter) {
res.columns.push(ArgColumn::Generic(vec![filter]));
} else {
error!("failed to parse filter '{}'", filter);
}
} else if arg == "--column" || arg == "-c" {
i += 1;
let column_name = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("column argument missing");
continue;
};
if let Some(rest) = column_name.strip_prefix("contacts:") {
if let Ok(pubkey) = Pubkey::parse(rest) {
info!("contact column for user {}", pubkey.hex());
res.columns
.push(ArgColumn::Timeline(TimelineKind::contact_list(pubkey)))
} else {
error!("error parsing contacts pubkey {}", rest);
continue;
}
} else if column_name == "contacts" {
if let Some(deck_author) = deck_author {
res.columns
.push(ArgColumn::Timeline(TimelineKind::contact_list(
deck_author.to_owned(),
)));
} else {
panic!(
"No accounts available, could not handle implicit pubkey contacts column"
);
}
} else if column_name == "search" {
i += 1;
let search = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("search filter argument missing?");
continue;
};
res.columns.push(ArgColumn::Timeline(TimelineKind::search(
search.to_string(),
)));
} else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") {
if let Ok(pubkey) = Pubkey::parse(notif_pk_str) {
info!("got notifications column for user {}", pubkey.hex());
res.columns
.push(ArgColumn::Timeline(TimelineKind::notifications(pubkey)));
} else {
error!("error parsing notifications pubkey {}", notif_pk_str);
continue;
}
} else if column_name == "notifications" {
debug!("got notification column for default user");
if let Some(deck_author) = deck_author {
res.columns
.push(ArgColumn::Timeline(TimelineKind::notifications(
deck_author.to_owned(),
)));
} else {
panic!("Tried to push notifications timeline with no available users");
}
} else if column_name == "profile" {
debug!("got profile column for default user");
if let Some(deck_author) = deck_author {
res.columns.push(ArgColumn::Timeline(TimelineKind::profile(
deck_author.to_owned(),
)));
} else {
panic!("Tried to push profile timeline with no available users");
}
} else if column_name == "universe" {
debug!("got universe column");
res.columns
.push(ArgColumn::Timeline(TimelineKind::Universe))
} else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
info!("got profile column for user {}", pubkey.hex());
res.columns
.push(ArgColumn::Timeline(TimelineKind::profile(pubkey)))
} else {
error!("error parsing profile pubkey {}", profile_pk_str);
continue;
}
}
} else if arg == "--filter-file" || arg == "-f" {
i += 1;
let filter_file = if let Some(next_arg) = args.get(i) {
next_arg
} else {
error!("filter file argument missing?");
continue;
};
let data = if let Ok(data) = std::fs::read(filter_file) {
data
} else {
error!("failed to read filter file '{}'", filter_file);
continue;
};
if let Some(filter) = std::str::from_utf8(&data)
.ok()
.and_then(|s| Filter::from_json(s).ok())
{
res.columns.push(ArgColumn::Generic(vec![filter]));
} else {
error!("failed to parse filter in '{}'", filter_file);
}
} else {
unrecognized_args.insert(arg.clone());
}
i += 1;
}
(res, unrecognized_args)
}
}
/// A way to define columns from the commandline. Can be column kinds or
/// generic queries
#[derive(Debug, Clone)]
pub enum ArgColumn {
Timeline(TimelineKind),
Generic(Vec<Filter>),
}
impl ArgColumn {
pub fn into_timeline_kind(self) -> TimelineKind {
match self {
ArgColumn::Generic(_filters) => {
// TODO: fix generic filters by referencing some filter map
TimelineKind::Generic(0)
}
ArgColumn::Timeline(tk) => tk,
}
}
}