args.rs (8179B)
1 use std::collections::BTreeSet; 2 3 use crate::timeline::TimelineKind; 4 use enostr::{Filter, Pubkey}; 5 use oot_bitset::{bitset_clear, bitset_get, bitset_set}; 6 use tracing::{debug, error, info}; 7 8 #[repr(u16)] 9 pub enum ColumnsFlag { 10 SinceOptimize, 11 Textmode, 12 Scramble, 13 NoMedia, 14 ShowNoteClientTop, 15 ShowNoteClientBottom, 16 } 17 18 pub struct ColumnsArgs { 19 pub columns: Vec<ArgColumn>, 20 flags: [u16; 2], 21 } 22 23 impl ColumnsArgs { 24 pub fn is_flag_set(&self, flag: ColumnsFlag) -> bool { 25 bitset_get(&self.flags, flag as u16) 26 } 27 28 pub fn set_flag(&mut self, flag: ColumnsFlag) { 29 bitset_set(&mut self.flags, flag as u16) 30 } 31 32 pub fn clear_flag(&mut self, flag: ColumnsFlag) { 33 bitset_clear(&mut self.flags, flag as u16) 34 } 35 36 pub fn parse(args: &[String], deck_author: Option<&Pubkey>) -> (Self, BTreeSet<String>) { 37 let mut unrecognized_args = BTreeSet::new(); 38 let mut res = Self { 39 columns: vec![], 40 flags: [0; 2], 41 }; 42 43 // flag defaults 44 res.set_flag(ColumnsFlag::SinceOptimize); 45 46 let mut i = 0; 47 let len = args.len(); 48 while i < len { 49 let arg = &args[i]; 50 51 if arg == "--textmode" { 52 res.set_flag(ColumnsFlag::Textmode); 53 } else if arg == "--no-since-optimize" { 54 res.clear_flag(ColumnsFlag::SinceOptimize); 55 } else if arg == "--scramble" { 56 res.set_flag(ColumnsFlag::Scramble); 57 } else if arg == "--show-note-client=top" { 58 res.set_flag(ColumnsFlag::ShowNoteClientTop); 59 } else if arg == "--show-note-client=bottom" { 60 res.set_flag(ColumnsFlag::ShowNoteClientBottom); 61 } else if arg == "--no-media" { 62 res.set_flag(ColumnsFlag::NoMedia); 63 } else if arg == "--filter" { 64 i += 1; 65 let filter = if let Some(next_arg) = args.get(i) { 66 next_arg 67 } else { 68 error!("filter argument missing?"); 69 continue; 70 }; 71 72 if let Ok(filter) = Filter::from_json(filter) { 73 res.columns.push(ArgColumn::Generic(vec![filter])); 74 } else { 75 error!("failed to parse filter '{}'", filter); 76 } 77 } else if arg == "--column" || arg == "-c" { 78 i += 1; 79 let column_name = if let Some(next_arg) = args.get(i) { 80 next_arg 81 } else { 82 error!("column argument missing"); 83 continue; 84 }; 85 86 if let Some(rest) = column_name.strip_prefix("contacts:") { 87 if let Ok(pubkey) = Pubkey::parse(rest) { 88 info!("contact column for user {}", pubkey.hex()); 89 res.columns 90 .push(ArgColumn::Timeline(TimelineKind::contact_list(pubkey))) 91 } else { 92 error!("error parsing contacts pubkey {}", rest); 93 continue; 94 } 95 } else if column_name == "contacts" { 96 if let Some(deck_author) = deck_author { 97 res.columns 98 .push(ArgColumn::Timeline(TimelineKind::contact_list( 99 deck_author.to_owned(), 100 ))); 101 } else { 102 panic!( 103 "No accounts available, could not handle implicit pubkey contacts column" 104 ); 105 } 106 } else if column_name == "search" { 107 i += 1; 108 let search = if let Some(next_arg) = args.get(i) { 109 next_arg 110 } else { 111 error!("search filter argument missing?"); 112 continue; 113 }; 114 115 res.columns.push(ArgColumn::Timeline(TimelineKind::search( 116 search.to_string(), 117 ))); 118 } else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") { 119 if let Ok(pubkey) = Pubkey::parse(notif_pk_str) { 120 info!("got notifications column for user {}", pubkey.hex()); 121 res.columns 122 .push(ArgColumn::Timeline(TimelineKind::notifications(pubkey))); 123 } else { 124 error!("error parsing notifications pubkey {}", notif_pk_str); 125 continue; 126 } 127 } else if column_name == "notifications" { 128 debug!("got notification column for default user"); 129 if let Some(deck_author) = deck_author { 130 res.columns 131 .push(ArgColumn::Timeline(TimelineKind::notifications( 132 deck_author.to_owned(), 133 ))); 134 } else { 135 panic!("Tried to push notifications timeline with no available users"); 136 } 137 } else if column_name == "profile" { 138 debug!("got profile column for default user"); 139 if let Some(deck_author) = deck_author { 140 res.columns.push(ArgColumn::Timeline(TimelineKind::profile( 141 deck_author.to_owned(), 142 ))); 143 } else { 144 panic!("Tried to push profile timeline with no available users"); 145 } 146 } else if column_name == "universe" { 147 debug!("got universe column"); 148 res.columns 149 .push(ArgColumn::Timeline(TimelineKind::Universe)) 150 } else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") { 151 if let Ok(pubkey) = Pubkey::parse(profile_pk_str) { 152 info!("got profile column for user {}", pubkey.hex()); 153 res.columns 154 .push(ArgColumn::Timeline(TimelineKind::profile(pubkey))) 155 } else { 156 error!("error parsing profile pubkey {}", profile_pk_str); 157 continue; 158 } 159 } 160 } else if arg == "--filter-file" || arg == "-f" { 161 i += 1; 162 let filter_file = if let Some(next_arg) = args.get(i) { 163 next_arg 164 } else { 165 error!("filter file argument missing?"); 166 continue; 167 }; 168 169 let data = if let Ok(data) = std::fs::read(filter_file) { 170 data 171 } else { 172 error!("failed to read filter file '{}'", filter_file); 173 continue; 174 }; 175 176 if let Some(filter) = std::str::from_utf8(&data) 177 .ok() 178 .and_then(|s| Filter::from_json(s).ok()) 179 { 180 res.columns.push(ArgColumn::Generic(vec![filter])); 181 } else { 182 error!("failed to parse filter in '{}'", filter_file); 183 } 184 } else { 185 unrecognized_args.insert(arg.clone()); 186 } 187 188 i += 1; 189 } 190 191 (res, unrecognized_args) 192 } 193 } 194 195 /// A way to define columns from the commandline. Can be column kinds or 196 /// generic queries 197 #[derive(Debug, Clone)] 198 pub enum ArgColumn { 199 Timeline(TimelineKind), 200 Generic(Vec<Filter>), 201 } 202 203 impl ArgColumn { 204 pub fn into_timeline_kind(self) -> TimelineKind { 205 match self { 206 ArgColumn::Generic(_filters) => { 207 // TODO: fix generic filters by referencing some filter map 208 TimelineKind::Generic(0) 209 } 210 ArgColumn::Timeline(tk) => tk, 211 } 212 } 213 }