use std::io::Write;
-use unicode_width::UnicodeWidthStr;
+use std::io::stderr;
+use std::process::ExitCode;
-use mastodonochrome::client::Client;
-use mastodonochrome::coloured_string::ColouredStringSlice;
-use mastodonochrome::text::{parse_html, Paragraph, TextFragment};
-
-use crossterm::{
- event::{self, Event, KeyCode, KeyEventKind},
- terminal::{
- disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
- LeaveAlternateScreen,
- },
- ExecutableCommand,
-};
-use ratatui::{
- prelude::{Buffer, CrosstermBackend, Terminal},
- style::{Style, Color, Modifier},
-};
-use std::io::stdout;
+use mastodonochrome::tui::Tui;
/*
#[allow(unused)]
}
*/
-fn ratatui_style_from_colour(colour: char) -> Style {
- match colour {
- // default
- ' ' => Style::default(),
-
- // message separator line, other than the date
- 'S' => Style::default().fg(Color::White).bg(Color::Blue)
- .add_modifier(Modifier::REVERSED | Modifier::BOLD),
-
- // date on a message separator line
- 'D' => Style::default().fg(Color::White).bg(Color::Blue)
- .add_modifier(Modifier::REVERSED),
-
- // username in a From line
- 'F' => Style::default().fg(Color::Green)
- .add_modifier(Modifier::BOLD),
-
- // username in other headers like Via
- 'f' => Style::default().fg(Color::Green),
-
- // <code> tags
- 'c' => Style::default().fg(Color::Yellow),
-
- // #hashtags
- '#' => Style::default().fg(Color::Cyan),
-
- // @mentions of a user
- '@' => Style::default().fg(Color::Green),
-
- // <em> tags
- '_' => Style::default().add_modifier(Modifier::UNDERLINED),
-
- // <strong> tags
- 's' => Style::default().add_modifier(Modifier::BOLD),
-
- // URL
- 'u' => Style::default().fg(Color::Blue)
- .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
-
- // media URL
- 'M' => Style::default().fg(Color::Magenta)
- .add_modifier(Modifier::BOLD),
-
- // media description
- 'm' => Style::default().fg(Color::Magenta),
-
- // Mastodonochrome logo in file headers
- 'J' => Style::default().fg(Color::Blue).bg(Color::White)
- .add_modifier(Modifier::REVERSED | Modifier::BOLD),
-
- // ~~~~~ underline in file headers
- '~' => Style::default().fg(Color::Blue),
-
- // actual header text in file headers
- 'H' => Style::default().fg(Color::Cyan),
-
- // keypress / keypath names in file headers
- 'K' => Style::default().fg(Color::Cyan)
- .add_modifier(Modifier::BOLD),
-
- // keypresses in file status lines
- 'k' => Style::default().add_modifier(Modifier::BOLD),
-
- // separator line between editor header and content
- '-' => Style::default().fg(Color::Cyan).bg(Color::Black)
- .add_modifier(Modifier::REVERSED),
-
- // something really boring, like 'none' in place of data
- '0' => Style::default().fg(Color::Blue),
-
- // red nastinesses like blocking/muting in Examine User
- 'r' => Style::default().fg(Color::Red),
-
- // # reverse-video > indicating a truncated too-long line
- '>' => Style::default().add_modifier(Modifier::REVERSED),
-
- // # error report, or by default any unrecognised colour character
- '!' | _ => Style::default().fg(Color::Red).bg(Color::Yellow).
- add_modifier(Modifier::REVERSED | Modifier::BOLD)
- }
-}
-
-fn ratatui_set_string(buf: &mut Buffer, y: usize, x: usize,
- text: &ColouredStringSlice<'_>) {
- let mut x = x;
- if let Ok(y) = y.try_into() {
- for (frag, colour) in text.frags() {
- if let Ok(x) = x.try_into() {
- buf.set_string(x, y, frag, ratatui_style_from_colour(colour));
- } else {
- break;
- }
- x += UnicodeWidthStr::width(frag);
- }
- }
-}
-
+/*
#[allow(unused)]
fn tui(paras: Vec<Paragraph>) -> std::io::Result<()> {
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
- let (sender, receiver) = std::sync::mpsc::sync_channel(1);
-
- let _term_input_thread = std::thread::spawn(move || {
- while let Ok(ev) = event::read() {
- if let Err(_) = sender.send(ev) {
- break;
- }
- }
- });
-
loop {
terminal.draw(|frame| {
let area = frame.size();
disable_raw_mode()?;
Ok(())
}
+*/
-fn main() {
+fn main() -> ExitCode {
+ /*
let mut client = Client::new().unwrap();
if let Some(st) = client.status_by_id("111602135142646031") {
let paras = parse_html(&st.content);
tui(paras).unwrap();
}
+ */
+
+ match Tui::run() {
+ Ok(_) => ExitCode::from(0),
+ Err(e) => {
+ let _ = writeln!(&mut stderr(), "mastodonochrome: error: {}", e);
+ ExitCode::from(1)
+ }
+ }
}
--- /dev/null
+use crossterm::{
+ event::{self, Event, KeyEvent, KeyCode, KeyEventKind},
+ terminal::{
+ disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
+ LeaveAlternateScreen,
+ },
+ ExecutableCommand,
+};
+use ratatui::{
+ prelude::{Buffer, CrosstermBackend, Rect, Terminal},
+ style::{Style, Color, Modifier},
+};
+use std::io::{Stdout, Write, stdout};
+use unicode_width::UnicodeWidthStr;
+
+use super::activity_stack::*;
+use super::client::Client;
+use super::coloured_string::{ColouredString, ColouredStringSlice};
+use super::text::{parse_html, Paragraph, TextFragment};
+
+fn ratatui_style_from_colour(colour: char) -> Style {
+ match colour {
+ // default
+ ' ' => Style::default(),
+
+ // message separator line, other than the date
+ 'S' => Style::default().fg(Color::White).bg(Color::Blue)
+ .add_modifier(Modifier::REVERSED | Modifier::BOLD),
+
+ // date on a message separator line
+ 'D' => Style::default().fg(Color::White).bg(Color::Blue)
+ .add_modifier(Modifier::REVERSED),
+
+ // username in a From line
+ 'F' => Style::default().fg(Color::Green)
+ .add_modifier(Modifier::BOLD),
+
+ // username in other headers like Via
+ 'f' => Style::default().fg(Color::Green),
+
+ // <code> tags
+ 'c' => Style::default().fg(Color::Yellow),
+
+ // #hashtags
+ '#' => Style::default().fg(Color::Cyan),
+
+ // @mentions of a user
+ '@' => Style::default().fg(Color::Green),
+
+ // <em> tags
+ '_' => Style::default().add_modifier(Modifier::UNDERLINED),
+
+ // <strong> tags
+ 's' => Style::default().add_modifier(Modifier::BOLD),
+
+ // URL
+ 'u' => Style::default().fg(Color::Blue)
+ .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
+
+ // media URL
+ 'M' => Style::default().fg(Color::Magenta)
+ .add_modifier(Modifier::BOLD),
+
+ // media description
+ 'm' => Style::default().fg(Color::Magenta),
+
+ // Mastodonochrome logo in file headers
+ 'J' => Style::default().fg(Color::Blue).bg(Color::White)
+ .add_modifier(Modifier::REVERSED | Modifier::BOLD),
+
+ // ~~~~~ underline in file headers
+ '~' => Style::default().fg(Color::Blue),
+
+ // actual header text in file headers
+ 'H' => Style::default().fg(Color::Cyan),
+
+ // keypress / keypath names in file headers
+ 'K' => Style::default().fg(Color::Cyan)
+ .add_modifier(Modifier::BOLD),
+
+ // keypresses in file status lines
+ 'k' => Style::default().add_modifier(Modifier::BOLD),
+
+ // separator line between editor header and content
+ '-' => Style::default().fg(Color::Cyan).bg(Color::Black)
+ .add_modifier(Modifier::REVERSED),
+
+ // something really boring, like 'none' in place of data
+ '0' => Style::default().fg(Color::Blue),
+
+ // red nastinesses like blocking/muting in Examine User
+ 'r' => Style::default().fg(Color::Red),
+
+ // # reverse-video > indicating a truncated too-long line
+ '>' => Style::default().add_modifier(Modifier::REVERSED),
+
+ // # error report, or by default any unrecognised colour character
+ '!' | _ => Style::default().fg(Color::Red).bg(Color::Yellow).
+ add_modifier(Modifier::REVERSED | Modifier::BOLD)
+ }
+}
+
+fn ratatui_set_string(buf: &mut Buffer, y: usize, x: usize,
+ text: &ColouredStringSlice<'_>) {
+ let mut x = x;
+ if let Ok(y) = y.try_into() {
+ for (frag, colour) in text.frags() {
+ if let Ok(x) = x.try_into() {
+ buf.set_string(x, y, frag, ratatui_style_from_colour(colour));
+ } else {
+ break;
+ }
+ x += UnicodeWidthStr::width(frag);
+ }
+ }
+}
+
+enum SubthreadEvent {
+ TermEv(Event),
+}
+
+enum HandleEventResult {
+ Nothing,
+ Beep,
+ Exit,
+}
+
+pub struct Tui {
+ terminal: Terminal<CrosstermBackend<Stdout>>,
+ subthread_sender: std::sync::mpsc::SyncSender<SubthreadEvent>,
+ subthread_receiver: std::sync::mpsc::Receiver<SubthreadEvent>,
+ state: TuiLogicalState,
+}
+
+impl Tui {
+ pub fn run() -> std::io::Result<()> {
+ stdout().execute(EnterAlternateScreen)?;
+ enable_raw_mode()?;
+ let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
+ terminal.clear()?;
+
+ let (sender, receiver) = std::sync::mpsc::sync_channel(1);
+
+ let input_sender = sender.clone();
+
+ // I don't think we have any need to join subthreads like this
+ let _joinhandle = std::thread::spawn(move || {
+ while let Ok(ev) = event::read() {
+ if let Err(_) = input_sender.send(SubthreadEvent::TermEv(ev)) {
+ break;
+ }
+ }
+ });
+
+ let mut tui = Tui {
+ terminal: terminal,
+ subthread_sender: sender,
+ subthread_receiver: receiver,
+ state: TuiLogicalState::new(),
+ };
+ let result = tui.main_loop();
+
+ disable_raw_mode()?;
+ stdout().execute(LeaveAlternateScreen)?;
+
+ result
+ }
+
+ fn main_loop(&mut self) -> std::io::Result<()> {
+ loop {
+ self.terminal.draw(|frame| {
+ let area = frame.size();
+ let buf = frame.buffer_mut();
+ self.state.draw_frame(area, buf);
+ })?;
+
+ match self.subthread_receiver.recv() {
+ e @ Err(_) => break Ok(()), // FIXME FIXME FIXME: not ok!
+ Ok(SubthreadEvent::TermEv(ev)) => {
+ match ev {
+ Event::Key(key) => {
+ if key.kind == KeyEventKind::Press {
+ match self.state.handle_keypress(key) {
+ HandleEventResult::Beep => Self::beep()?,
+
+ // FIXME: errors?
+ HandleEventResult::Exit => break Ok(()),
+
+ HandleEventResult::Nothing => (),
+ }
+ }
+ },
+ _ => (),
+ }
+ },
+ }
+ }
+ }
+
+ fn beep() -> std::io::Result<()> {
+ stdout().write(b"\x07")?;
+ Ok(())
+ }
+}
+
+struct TuiLogicalState {
+ activity_stack: ActivityStack,
+}
+
+impl TuiLogicalState {
+ fn new() -> Self {
+ TuiLogicalState {
+ activity_stack: ActivityStack::new(),
+ }
+ }
+
+ fn draw_frame(&self, _area: Rect, buf: &mut Buffer) {
+ buf.reset();
+ ratatui_set_string(buf, 2, 4, &ColouredString::general(
+ "#HelloWorld from Mastodonochrome",
+ "########### ",
+ ).slice());
+ }
+
+ fn handle_keypress(&mut self, key: KeyEvent) -> HandleEventResult {
+ match key.code {
+ KeyCode::Char('q') => HandleEventResult::Exit,
+ KeyCode::Char('b') => HandleEventResult::Beep,
+ _ => HandleEventResult::Nothing,
+ }
+ }
+}