A file-based task manager

FIX: multiple styles on one line breaking rendering

+77 -52
+1 -1
Cargo.lock
··· 582 582 583 583 [[package]] 584 584 name = "tsk" 585 - version = "0.2.1" 585 + version = "0.2.2" 586 586 dependencies = [ 587 587 "clap", 588 588 "clap_complete",
+1 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "tsk" 3 - version = "0.2.1" 3 + version = "0.2.2" 4 4 edition = "2021" 5 5 publish = true 6 6
+14 -7
src/main.rs
··· 96 96 /// Shows raw file attributes for the file 97 97 #[arg(short = 'x', default_value_t = false)] 98 98 show_attrs: bool, 99 + 100 + #[arg(short = 'R', default_value_t = false)] 101 + raw: bool, 99 102 /// The [TSK-]ID of the task to display 100 103 #[command(flatten)] 101 104 task_id: TaskId, ··· 232 235 Commands::Swap => command_swap(dir), 233 236 Commands::Show { 234 237 task_id, 238 + raw, 235 239 show_attrs, 236 - } => command_show(dir, task_id, show_attrs), 240 + } => command_show(dir, task_id, show_attrs, raw), 237 241 Commands::Follow { 238 242 task_id, 239 243 link_index, ··· 385 389 Workspace::from_path(dir)?.deprioritize(task_id.into()) 386 390 } 387 391 388 - fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool) -> Result<()> { 392 + fn command_show(dir: PathBuf, task_id: TaskId, show_attrs: bool, raw: bool) -> Result<()> { 389 393 let task = Workspace::from_path(dir)?.task(task_id.into())?; 390 394 // YAML front-matter style. YAML is gross, but it's what everyone uses! 391 395 if show_attrs && !task.attributes.is_empty() { ··· 395 399 } 396 400 println!("---"); 397 401 } 398 - if let Some(styled_task) = task::parse(&task.to_string()) { 399 - writeln!(io::stdout(), "{}", styled_task.content)?; 400 - } else { 401 - println!("{task}"); 402 + match task::parse(&task.to_string()) { 403 + Some(styled_task) if !raw => { 404 + writeln!(io::stdout(), "{}", styled_task.content)?; 405 + } 406 + _ => { 407 + println!("{task}"); 408 + } 402 409 } 403 410 Ok(()) 404 411 } ··· 421 428 if edit { 422 429 command_edit(dir, taskid) 423 430 } else { 424 - command_show(dir, taskid, false) 431 + command_show(dir, taskid, false, false) 425 432 } 426 433 } 427 434 }
+61 -43
src/task.rs
··· 9 9 #[derive(Debug, Eq, PartialEq, Clone, Copy)] 10 10 enum ParserState { 11 11 // Started by ` =`, terminated by `= 12 - Highlight(usize), 12 + Highlight(usize, usize), 13 13 // Started by ` [`, terminated by `](` 14 - Linktext(usize), 14 + Linktext(usize, usize), 15 15 // Started by `](`, terminated by `) `, must immedately follow a Linktext 16 - Link(usize), 17 - RawLink(usize), 16 + Link(usize, usize), 17 + RawLink(usize, usize), 18 18 // Started by ` [[`, terminated by `]] ` 19 - InternalLink(usize), 19 + InternalLink(usize, usize), 20 20 // Started by ` *`, terminated by `* ` 21 - Italics(usize), 21 + Italics(usize, usize), 22 22 // Started by ` !`, termianted by `!` 23 - Bold(usize), 23 + Bold(usize, usize), 24 24 // Started by ` _`, terminated by `_ ` 25 - Underline(usize), 25 + Underline(usize, usize), 26 26 // Started by ` -`, terminated by `- ` 27 - Strikethrough(usize), 27 + Strikethrough(usize, usize), 28 28 29 29 // TODO: implement these. 30 30 // Started by `_ `, terminated by `_` ··· 37 37 // `\n` and followed by a `\n` 38 38 BlockEnd(usize), 39 39 // Started by ` ``, terminated by `` ` or `\n` 40 - InlineBlock(usize), 40 + InlineBlock(usize, usize), 41 41 // Started by `^\w+>`, terminated by `\n` 42 42 Blockquote(usize), 43 43 } ··· 64 64 let state_last = state.last().cloned(); 65 65 match stream.next() { 66 66 // there will always be an op code in the stack 67 - Some((_, c)) => { 67 + Some((char_pos, c)) => { 68 68 out.push(c); 69 69 let end = out.len() - 1; 70 70 match (last, c, state_last) { 71 71 ('[', '[', _) => { 72 - state.push(InternalLink(end)); 72 + state.push(InternalLink(end, char_pos)); 73 73 } 74 - (']', ']', Some(InternalLink(il))) => { 74 + (']', ']', Some(InternalLink(il, s_pos))) => { 75 75 state.pop(); 76 - let contents = out.get(il + 1..out.len() - 2)?; 76 + let contents = s.get(s_pos + 1..char_pos - 1)?; 77 77 if let Ok(id) = Id::from_str(&contents) { 78 78 let linktext = format!( 79 79 "{}{}", ··· 87 87 } 88 88 } 89 89 (' ' | '\r' | '\n', '[', _) => { 90 - state.push(Linktext(end)); 90 + state.push(Linktext(end, char_pos)); 91 91 } 92 - (']', '(', Some(Linktext(_))) => { 93 - state.push(Link(end)); 92 + (']', '(', Some(Linktext(_, _))) => { 93 + state.push(Link(end, char_pos)); 94 94 } 95 - (')', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Link(_))) => { 96 - let linkpos = if let Link(lp) = state.pop().unwrap() { 95 + (')', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Link(_, _))) => { 96 + // TODO: this needs to be updated to use `s` instead of `out` for position 97 + // parsing 98 + let linkpos = if let Link(lp, _) = state.pop().unwrap() { 97 99 lp 98 100 } else { 99 101 // remove the linktext state, it is always present. 100 102 state.pop(); 101 103 continue; 102 104 }; 103 - let linktextpos = if let Linktext(lt) = state.pop().unwrap() { 105 + let linktextpos = if let Linktext(lt, _) = state.pop().unwrap() { 104 106 lt 105 107 } else { 106 108 continue; ··· 116 118 out.replace_range(linktextpos..end, &linktext); 117 119 } 118 120 } 119 - ('>', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(RawLink(hl))) => { 121 + ('>', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(RawLink(hl, s_pos))) 122 + if s_pos != char_pos - 1 => 123 + { 120 124 state.pop(); 121 - let link = out.get(hl + 1..out.len() - 2)?; 125 + let link = s.get(s_pos + 1..char_pos - 1)?; 122 126 if let Ok(url) = Url::parse(link) { 123 127 let linktext = 124 128 format!("{}{}", link.blue(), super_num(links.len() + 1).purple()); ··· 127 131 } 128 132 } 129 133 (' ' | '\r' | '\n', '<', _) => { 130 - state.push(RawLink(end)); 134 + state.push(RawLink(end, char_pos)); 131 135 } 132 - ('=', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Highlight(hl))) => { 136 + ('=', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Highlight(hl, s_pos))) 137 + if s_pos != char_pos - 1 => 138 + { 133 139 state.pop(); 134 140 out.replace_range( 135 141 hl..end, 136 - &out.get(hl + 1..out.len() - 2)?.reversed().to_string(), 142 + &s.get(s_pos + 1..char_pos - 1)?.reversed().to_string(), 137 143 ); 138 144 } 139 145 (' ' | '\r' | '\n', '=', _) => { 140 - state.push(Highlight(end)); 146 + state.push(Highlight(end, char_pos)); 141 147 } 142 148 (' ' | '\r' | '\n', '*', _) => { 143 - state.push(Italics(end)); 149 + state.push(Italics(end, char_pos)); 144 150 } 145 - ('*', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Italics(il))) => { 151 + ('*', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Italics(il, s_pos))) 152 + if s_pos != char_pos - 1 => 153 + { 146 154 state.pop(); 147 155 out.replace_range( 148 156 il..end, 149 - &out.get(il + 1..out.len() - 2)?.italic().to_string(), 157 + &s.get(s_pos + 1..char_pos - 1)?.italic().to_string(), 150 158 ); 151 159 } 152 160 (' ' | '\r' | '\n', '!', _) => { 153 - state.push(Bold(end)); 161 + state.push(Bold(end, char_pos)); 154 162 } 155 - ('!', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Bold(il))) => { 163 + ('!', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Bold(il, s_pos))) 164 + if s_pos != char_pos - 1 => 165 + { 156 166 state.pop(); 157 - out.replace_range(il..end, &out.get(il + 1..end - 1)?.bold().to_string()); 167 + out.replace_range( 168 + il..end, 169 + &s.get(s_pos + 1..char_pos - 1)?.bold().to_string(), 170 + ); 158 171 } 159 172 (' ' | '\r' | '\n', '_', _) => { 160 - state.push(Underline(end)); 173 + state.push(Underline(end, char_pos)); 161 174 } 162 - ('_', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Underline(il))) => { 175 + ('_', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Underline(il, s_pos))) 176 + if s_pos != char_pos - 1 => 177 + { 163 178 state.pop(); 164 179 out.replace_range( 165 180 il..end, 166 - &out.get(il + 1..end - 1)?.underline().to_string(), 181 + &s.get(s_pos + 1..char_pos - 1)?.underline().to_string(), 167 182 ); 168 183 } 169 184 (' ' | '\r' | '\n', '~', _) => { 170 - state.push(Strikethrough(end)); 185 + state.push(Strikethrough(end, char_pos)); 171 186 } 172 - ('~', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Strikethrough(il))) => { 187 + ('~', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Strikethrough(il, s_pos))) 188 + if s_pos != char_pos - 1 => 189 + { 173 190 state.pop(); 174 191 out.replace_range( 175 192 il..end, 176 - &out.get(il + 1..end - 1)?.strikethrough().to_string(), 193 + &s.get(s_pos + 1..char_pos - 1)?.strikethrough().to_string(), 177 194 ); 178 195 } 179 - ('`', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(InlineBlock(hl))) => { 180 - state.pop(); 196 + ('`', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(InlineBlock(hl, s_pos))) 197 + if s_pos != char_pos - 1 => 198 + { 181 199 out.replace_range( 182 200 hl..end, 183 - &out.get(hl + 1..out.len() - 1)?.green().to_string(), 201 + &s.get(s_pos + 1..char_pos - 1)?.green().to_string(), 184 202 ); 185 203 } 186 - (' ' | '\r' | '\n', '`', _) => { 187 - state.push(InlineBlock(end)); 204 + (' ' | '\n' | '\r' | '.' | '!' | '?', '`', _) => { 205 + state.push(InlineBlock(end, char_pos)); 188 206 } 189 207 _ => (), 190 208 }