ui.rs 16.7 KB
Newer Older
zyno42's avatar
zyno42 committed
1
use std::{io::stdout, sync::mpsc, thread, time::Duration};
2

zyno42's avatar
zyno42 committed
3
use log::trace;
zyno42's avatar
zyno42 committed
4

5
6
use tui::{
    backend::{Backend, CrosstermBackend},
zyno42's avatar
zyno42 committed
7
8
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
9
    symbols::DOT,
zyno42's avatar
zyno42 committed
10
11
12
    text::{Span, Spans, Text},
    widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Tabs, Wrap},
    Frame, Terminal,
13
14
15
};

use crossterm::{
zyno42's avatar
zyno42 committed
16
    event::{self, Event as CEvent, KeyCode, KeyEvent, KeyModifiers},
17
    execute,
zyno42's avatar
zyno42 committed
18
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
19
20
};

21
22
use unicode_width::UnicodeWidthStr;

23
24
25
26
27
use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
pub enum Error {
    #[snafu(display("Failure in Crossterm Terminal Backend: {}", source))]
zyno42's avatar
zyno42 committed
28
    Crossterm { source: std::io::Error },
29
30
31
32
33
34
35
36
37
38

    #[snafu(display("Failed to receive Input event: {}", source))]
    ReceiveInput { source: std::sync::mpsc::RecvError },

    #[snafu(display("Failed to handle Input event: {}", source))]
    HandleInput { source: std::io::Error },
}

pub type Result<T, E = Error> = std::result::Result<T, E>;

zyno42's avatar
zyno42 committed
39
use crate::app::{AccessMode, App, InputFrame, InputMode};
40

zyno42's avatar
zyno42 committed
41
// Define an Event which can consist of a pressed key or the terminal got resized.
42
43
44
enum Event<I, H> {
    Input(I),
    Resize(H, H),
zyno42's avatar
zyno42 committed
45
    // TODO: Maybe add a tick event which occurs when the UI should be updated while no key got pressed?
46
47
48
49
}

pub fn setup(app: &mut App) -> Result<()> {
    // Raw mode disables some common terminal functions that are unnecessary in the TUI environment
50
    enable_raw_mode().context(CrosstermSnafu {})?;
51
52
53

    // Enter the Alternate Screen, so we don't break terminal history (it's like opening vim)
    let mut stdout = stdout();
54
    execute!(stdout, EnterAlternateScreen).context(CrosstermSnafu {})?;
55
56
57

    // Initialize Crossterm backend
    let backend = CrosstermBackend::new(stdout);
58
    let mut terminal = Terminal::new(backend).context(CrosstermSnafu {})?;
59
60

    // Clear the Alternate Screen if someone left it dirty
61
    terminal.clear().context(CrosstermSnafu {})?;
62
63
64
65
66

    // Save the result of the main loop to return it after tearing down the backend
    let result = run_event_loop(app, &mut terminal);

    // Leave Alternate Screen to shut down cleanly regardless of the result
67
68
69
    disable_raw_mode().context(CrosstermSnafu {})?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen).context(CrosstermSnafu {})?;
    terminal.show_cursor().context(CrosstermSnafu {})?;
70
71
72
73
74
75
76

    // Return the result of the main loop after restoring the previous terminal state in order to
    // not be stuck in the Alternate Screen / or Raw Mode which would make a `reset` of the shell
    // necessary
    result
}

zyno42's avatar
zyno42 committed
77
fn run_event_loop<B: Backend>(app: &mut App, terminal: &mut Terminal<B>) -> Result<()> {
78
79
    // Setup input handling as in the crossterm demo with a multi producer single consumer (mpsc) channel
    let (tx, rx) = mpsc::channel();
zyno42's avatar
zyno42 committed
80
81
82
83
84
85
86
87
88
89
90
91
92
    thread::spawn(move || loop {
        if event::poll(Duration::from_millis(250)).expect("Event loop: could not poll for events!")
        {
            if let CEvent::Key(key) =
                event::read().expect("Event loop: could not read a key event!")
            {
                tx.send(Event::Input(key))
                    .expect("Event loop: could not send an input event!");
            } else if let CEvent::Resize(w, h) =
                event::read().expect("Event loop: could not read a resize event!")
            {
                tx.send(Event::Resize(w, h))
                    .expect("Event loop: could not send a resize event!");
93
94
95
96
97
98
99
            }
        }
    });

    // Event loop
    loop {
        // Update UI
zyno42's avatar
zyno42 committed
100
        draw(app, terminal)?;
101
102

        // Handle events
103
        match rx.recv().context(ReceiveInputSnafu {})? {
104
            // Match key pressed events
105
            Event::Input(event) => {
zyno42's avatar
zyno42 committed
106
107
108
109
                trace!("Input event: {:?}", event);

                // Match the input mode, either you're in line input mode where you enter new
                // values for registers or you're in the default navigation mode.
110
111
112
                match app.input_mode {
                    InputMode::Edit => match event.code {
                        KeyCode::Char(c) => app.input.push(c),
zyno42's avatar
zyno42 committed
113
114
                        KeyCode::Backspace => {
                            if app.input.pop().is_none() {
zyno42's avatar
zyno42 committed
115
                                app.input_mode = InputMode::Navigation;
zyno42's avatar
zyno42 committed
116
117
                            }
                        }
118
119
120
121
                        KeyCode::Enter => app.on_enter(),
                        KeyCode::Esc => app.on_escape(),
                        _ => {}
                    },
zyno42's avatar
zyno42 committed
122
                    InputMode::Navigation => match event {
zyno42's avatar
zyno42 committed
123
                        // Press 'Shift+Tab' to switch backward through tabs
124
125
                        KeyEvent {
                            modifiers: KeyModifiers::SHIFT,
zyno42's avatar
zyno42 committed
126
127
                            code: KeyCode::BackTab,
                        } => app.previous_tab(),
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
                        // without any modifiers
                        KeyEvent {
                            modifiers: KeyModifiers::NONE,
                            code,
                        } => match code {
                            // Press 'q' to quit application
                            KeyCode::Char('q') => return Ok(()),
                            // Press 'r' to redraw the UI
                            KeyCode::Char('r') => continue,
                            // Press 'Tab' to switch forward through tabs
                            KeyCode::Tab => app.next_tab(),
                            // Press ↑ or 'k' to go up in the list of PEs/Registers
                            KeyCode::Up | KeyCode::Char('k') => app.on_up(),
                            // Press ↓ or 'j' to go down in the list of PEs/Registers
                            KeyCode::Down | KeyCode::Char('j') => app.on_down(),
                            // Press Escape or 'h' to return back to the list of PEs
                            KeyCode::Esc | KeyCode::Left | KeyCode::Char('h') => app.on_escape(),
                            // Press Enter or 'l' to select a PE/Register
                            KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => app.on_enter(),
                            // Press 's' on a selected PE to start a job
148
                            KeyCode::Char('s') => match app.access_mode {
zyno42's avatar
zyno42 committed
149
150
                                AccessMode::Unsafe {} => app.messages.push(app.start_current_pe()),
                                AccessMode::Monitor {} => app.messages.push("Unsafe access mode necessary to start a PE. Restart the app with `unsafe` parameter.".to_string())
151
                            },
zyno42's avatar
zyno42 committed
152
                            _ => {}
153
                        },
zyno42's avatar
zyno42 committed
154
                        _ => {}
155
156
                    },
                }
zyno42's avatar
zyno42 committed
157
            }
158
            // TODO: When opening a new pane in Tmux this app is not redrawn.
159
160
161
162
163
164
165
166
167
168
169
            Event::Resize(_, _) => continue,
        }
    }
}

fn draw<B: Backend>(app: &mut App, terminal: &mut Terminal<B>) -> Result<()> {
    terminal.draw(|f| {
        // Create a layout with fixed space for the Tab bar and a flexible space where each tab can
        // draw itself
        let tabs_chunks = Layout::default()
            .direction(Direction::Vertical)
170
            .margin(0)
171
172
            .constraints(
                [
173
                    Constraint::Length(2),
174
175
176
177
178
179
                    Constraint::Length(3),
                    Constraint::Min(0),
                ].as_ref()
            )
            .split(f.size());

180
181
182
183
184
185
186
187
188
189
190
        // Render title and general user instructions
        f.render_widget(
            Paragraph::new(
                Spans::from(vec![
                            Span::raw(&app.title),
                            Span::raw(" (q: Quit. "),
                            Span::styled("Remember to quit before reprogramming the FPGA or reloading the kernel module!",
                                         Style::default().add_modifier(Modifier::ITALIC)),
                                         Span::raw(")"),
                ])), tabs_chunks[0]);

191
192
193
194
195
196
197
198
        // Map the titles of the Tabs into Spans to be able to highlight the title of the
        // selected Tab
        let titles = app.tabs.titles
            .iter()
            .map(|t| Spans::from(Span::styled(*t, Style::default())))
            .collect();
        let tabs = Tabs::new(titles)
            .block(Block::default()
zyno42's avatar
zyno42 committed
199
                .title(Span::styled("Tabs (Shift+Tab: \u{2190}, Tab: \u{2192})",
200
201
202
203
204
205
                                    Style::default().add_modifier(Modifier::DIM)))
                .border_type(BorderType::Rounded)
                .border_style(Style::default().add_modifier(Modifier::DIM))
                .borders(Borders::ALL))
            .style(Style::default()
                .fg(Color::White))
206
            .highlight_style(Style::default().add_modifier(Modifier::BOLD))
207
208
209
            .divider(DOT)
            .select(app.tabs.index);

210
        f.render_widget(tabs, tabs_chunks[1]);
211
212
213

        // Call the specific draw function for the selected Tab
        match app.tabs.index {
214
215
216
            0 => draw_tab_peek_and_poke_pes(f, app, tabs_chunks[2]),
            1 => draw_tab_platform_components(f, app, tabs_chunks[2]),
            2 => draw_tab_bitstream_and_device_info(f, app, tabs_chunks[2]),
217
218
            _ => {},
        }
219
    }).context(CrosstermSnafu {})?;
220
221
222
223

    Ok(())
}

224
fn draw_tab_peek_and_poke_pes<B: Backend>(f: &mut Frame<B>, app: &mut App, chunk: Rect) {
225
    // Create a vertical layout (top to bottom) first to split the Tab into 3 rows with a
zyno42's avatar
zyno42 committed
226
227
    // bottom line for keyboard input that is only shown when in Edit Mode (that then replaces
    // the messages view):
228
229
    let vertical_chunks = Layout::default()
        .direction(Direction::Vertical)
230
231
232
233
234
        .margin(0)
        .constraints(match app.input_mode {
            InputMode::Edit => [
                Constraint::Length(15),
                Constraint::Min(30),
235
                Constraint::Length(3),
zyno42's avatar
zyno42 committed
236
237
            ]
            .as_ref(),
zyno42's avatar
zyno42 committed
238
239
240
241
242
243
            InputMode::Navigation => [
                Constraint::Length(15),
                Constraint::Min(30),
                Constraint::Length(15),
            ]
            .as_ref(),
244
        })
245
246
        .split(chunk);

247
248
    // Split the second row into half horizontally
    let horizontal_chunks = Layout::default()
249
250
        .direction(Direction::Horizontal)
        .margin(0)
zyno42's avatar
zyno42 committed
251
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
252
253
254
255
        .split(vertical_chunks[1]);

    // Draw the PEs as stateful list to be able to select one
    let pes_title = if (app.access_mode == AccessMode::Monitor {}) {
zyno42's avatar
zyno42 committed
256
        "PE List (j:\u{2193}, k:\u{2191})"
257
    } else {
zyno42's avatar
zyno42 committed
258
        "PE List (j:\u{2193}, k:\u{2191}, s: start the selected PE, Enter/l: switch to Register List)"
259
    };
zyno42's avatar
zyno42 committed
260
261
262
263
    let pes: Vec<ListItem> = app
        .pe_infos
        .items
        .iter()
264
265
266
        .map(|i| ListItem::new(vec![Spans::from(Span::raw(i))]))
        .collect();
    let pes = List::new(pes)
zyno42's avatar
zyno42 committed
267
268
269
270
        .block(focusable_block(
            pes_title,
            app.focus == InputFrame::PEList {},
        ))
271
272
273
        .highlight_style(Style::default().add_modifier(Modifier::BOLD))
        .highlight_symbol("> ");
    f.render_stateful_widget(pes, vertical_chunks[0], &mut app.pe_infos.state);
274

275
276
    // Split the PE's registers into status plus return value and arguments
    let register_chunks = Layout::default()
277
278
        .direction(Direction::Vertical)
        .margin(0)
zyno42's avatar
zyno42 committed
279
        .constraints([Constraint::Length(5), Constraint::Min(10)].as_ref())
280
281
282
        .split(horizontal_chunks[0]);

    // Status registers
zyno42's avatar
zyno42 committed
283
284
285
286
287
288
    draw_block_with_paragraph(
        f,
        "Status Registers",
        app.get_status_registers(),
        register_chunks[0],
    );
289
290
291
292
293

    // Argument Register List (also stateful list for editing)
    let registers_title = if (app.access_mode == AccessMode::Monitor {}) {
        "Register List (r: Refresh)"
    //} else if (app.access_mode == AccessMode::Debug {}) {
zyno42's avatar
zyno42 committed
294
    //    "Register List (r: Refresh, Escape: back, j:\u{2193}, k:\u{2191}, Enter/l: set Register, s: Start PE)"
295
    } else {
zyno42's avatar
zyno42 committed
296
        "Register List (r: Refresh, Escape: back, j:\u{2193}, k:\u{2191}, Enter/l: set Register)"
297
    };
298
    let registers = app.get_argument_registers(register_chunks[1].height.saturating_sub(2).into());
zyno42's avatar
zyno42 committed
299
300
    let registers: Vec<ListItem> = registers
        .iter()
301
302
303
        .map(|i| ListItem::new(vec![Spans::from(Span::raw(i))]))
        .collect();
    let registers = List::new(registers)
zyno42's avatar
zyno42 committed
304
305
306
307
        .block(focusable_block(
            registers_title,
            app.focus == InputFrame::RegisterList,
        ))
308
309
        .highlight_style(Style::default().add_modifier(Modifier::BOLD))
        .highlight_symbol("> ");
310
    f.render_stateful_widget(registers, register_chunks[1], &mut app.register_list);
311

312
    // TODO: Should local memory also be editable?
zyno42's avatar
zyno42 committed
313
314
    let local_memory =
        app.dump_current_pe_local_memory(horizontal_chunks[1].height.saturating_sub(2).into());
zyno42's avatar
zyno42 committed
315
316
    let local_memory: Vec<ListItem> = local_memory
        .iter()
317
318
319
320
321
322
        .map(|i| ListItem::new(vec![Spans::from(Span::raw(i))]))
        .collect();
    let local_memory = List::new(local_memory)
        .block(focusable_block("Local Memory (r: Refresh)", false))
        .highlight_style(Style::default().add_modifier(Modifier::BOLD))
        .highlight_symbol("> ");
zyno42's avatar
zyno42 committed
323
324
325
    f.render_stateful_widget(
        local_memory,
        horizontal_chunks[1],
326
        &mut app.local_memory_list,
zyno42's avatar
zyno42 committed
327
    );
328

329
    // Draw an input line if in Edit Mode or the messages view when not in Edit Mode
330
    if app.input_mode == InputMode::Edit {
zyno42's avatar
zyno42 committed
331
332
333
334
335
336
337
        let input = Paragraph::new(app.input.as_ref()).block(
            Block::default()
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .title("Input (Escape: abort, Enter: try to parse input as signed decimal i64)")
                .style(Style::default().fg(Color::Yellow)),
        );
338
339
340
341
342
343
        let input_chunks = vertical_chunks[2];
        f.render_widget(input, input_chunks);

        // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
        f.set_cursor(
            // Put cursor past the end of the input text
344
            input_chunks.x + (app.input.width() + 1).try_into().unwrap_or(0),
345
            // Move one line down, from the border to the input line
zyno42's avatar
zyno42 committed
346
347
            input_chunks.y + 1,
        );
348
349
350
351
    } else {
        draw_block_with_paragraph(
            f,
            "Messages",
zyno42's avatar
zyno42 committed
352
353
354
355
356
357
358
359
            app.messages
                .iter()
                .rev()
                .take(vertical_chunks[2].height.saturating_sub(2).into())
                .rev()
                .cloned()
                .collect::<Vec<String>>()
                .join("\n"),
360
361
            vertical_chunks[2],
        );
362
363
364
365
366
367
368
    }
}

fn draw_tab_platform_components<B: Backend>(f: &mut Frame<B>, app: &App, chunk: Rect) {
    // Create a vertical layout (top to bottom) first to split the Tab into 2 rows
    let vertical_chunks = Layout::default()
        .direction(Direction::Vertical)
369
        .margin(0)
zyno42's avatar
zyno42 committed
370
        .constraints([Constraint::Min(15), Constraint::Length(10)].as_ref())
371
        .split(chunk);
372

373
374
375
376
    // Show general info about platform components
    draw_block_with_paragraph(f, "Overview", app.get_platform_info(), vertical_chunks[0]);

    // and DMAEngine Statistics
zyno42's avatar
zyno42 committed
377
378
379
380
381
382
    draw_block_with_paragraph(
        f,
        "DMAEngine Statistics (r: Refresh)",
        app.get_dmaengine_statistics(),
        vertical_chunks[1],
    );
383
384
}

385
386
fn draw_tab_bitstream_and_device_info<B: Backend>(f: &mut Frame<B>, app: &App, chunk: Rect) {
    draw_block_with_paragraph(f, "", app.get_bitstream_info(), chunk);
387
388
389
}

/// Draw a block with some text in it into the rectangular space given by chunk
zyno42's avatar
zyno42 committed
390
391
392
393
394
395
396
397
fn draw_block_with_paragraph<'a, B: Backend, T>(
    f: &mut Frame<B>,
    block_title: &str,
    text: T,
    chunk: Rect,
) where
    Text<'a>: From<T>,
{
398
    let block = dim_block(block_title);
zyno42's avatar
zyno42 committed
399
    let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: false });
400
401
402
    f.render_widget(paragraph, chunk);
}

403
404
/// Create a new Block with round corners and the given title and choose the border style
fn block_with_border_style(title: &str, style: Modifier) -> Block {
405
    Block::default()
406
        .title(Span::styled(title, Style::default().add_modifier(style)))
407
        .border_type(BorderType::Rounded)
408
        .border_style(Style::default().add_modifier(style))
409
410
        .borders(Borders::ALL)
}
411
412
413
414
415
416
417
418

/// Create a new Block with round corners in dim colors and the given title
fn dim_block(title: &str) -> Block {
    block_with_border_style(title, Modifier::DIM)
}

/// Create a new Block with round corners which takes a boolean if it is focused
fn focusable_block(title: &str, focused: bool) -> Block {
zyno42's avatar
zyno42 committed
419
420
421
422
423
424
425
426
    block_with_border_style(
        title,
        if focused {
            Modifier::BOLD
        } else {
            Modifier::DIM
        },
    )
427
}