A file-based task manager

FIX: multiple styles on one line breaking rendering

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