Create App struct for TUI.
This commit is contained in:
parent
7fe3e3e14d
commit
fac6ea6bcd
21
README.md
21
README.md
@ -1,8 +1,18 @@
|
|||||||
# CLIDE
|
# CLIDE
|
||||||
|
|
||||||
CLIDE is an IDE written in Rust that supports both full and headless Linux environments.
|
CLIDE is a barebones but extendable IDE written in Rust using the Qt UI framework that supports both full and headless Linux environments.
|
||||||
|
The core application will provide you with a text editor that can be extended with plugins written in Rust.
|
||||||
|
|
||||||
|
The UI is written in QML and compiled to C++ using `cxx`, which is then linked into the Rust application.
|
||||||
|
|
||||||
|
It's up to you to build your own development environment for your tools.
|
||||||
|
This project is intended to be a light-weight core application with no language-specific tools or features.
|
||||||
|
To add tools for your purposes, create a plugin that implements the `ClidePlugin` trait. (This is currently under development and not yet available.)
|
||||||
|
Once you've created your plugin, you can submit a pull request to add your plugin to the final section in this README if you'd like to contribute.
|
||||||
|
If this section becomes too large, we may explore other options to distribute plugins.
|
||||||
|
|
||||||
The following packages must be installed before the application will build.
|
The following packages must be installed before the application will build.
|
||||||
|
In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs qt6-svg-dev
|
sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs qt6-svg-dev
|
||||||
@ -22,8 +32,8 @@ The [Qt Installer](https://www.qt.io/download-qt-installer) will provide the lat
|
|||||||
If using RustRover be sure to set your QML binaries path in the settings menu.
|
If using RustRover be sure to set your QML binaries path in the settings menu.
|
||||||
If Qt was installed to its default directory this will be `$HOME/Qt/6.8.3/gcc_64/bin/`.
|
If Qt was installed to its default directory this will be `$HOME/Qt/6.8.3/gcc_64/bin/`.
|
||||||
|
|
||||||
Viewing documentation in the web browser is possible, but you will end up in a mess of tabs.
|
Viewing documentation in the web browser is possible, but using Qt Assistant is recommended.
|
||||||
Using Qt Assistant is recommended. It comes with Qt6 when installed. Run the following command to start it.
|
It comes with Qt6 when installed. Run the following command to start it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nohup $HOME/Qt/6.8.3/gcc_64/bin/assistant > /dev/null 2>&1 &
|
nohup $HOME/Qt/6.8.3/gcc_64/bin/assistant > /dev/null 2>&1 &
|
||||||
@ -58,3 +68,8 @@ Some helpful links for reading up on QML if you're just getting started.
|
|||||||
* [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html)
|
* [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html)
|
||||||
* [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/)
|
* [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/)
|
||||||
* [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt)
|
* [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt)
|
||||||
|
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
TODO: Add a list of plugins here. The first example will be C++ with CMake functionality.
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@ -8,8 +8,8 @@ pub mod gui;
|
|||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|
||||||
/// Command line interface IDE with full GUI and headless modes.
|
/// Command line interface IDE with full GUI and headless modes.
|
||||||
/// If no flags are provided the GUI editor is launched in a separate process.
|
/// If no flags are provided, the GUI editor is launched in a separate process.
|
||||||
/// If no path is provided the current directory is used.
|
/// If no path is provided, the current directory is used.
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
#[structopt(name = "clide", verbatim_doc_comment)]
|
#[structopt(name = "clide", verbatim_doc_comment)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@ -30,11 +30,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let args = Cli::from_args();
|
let args = Cli::from_args();
|
||||||
|
|
||||||
let root_path = match args.path {
|
let root_path = match args.path {
|
||||||
// If the CLI was provided a directory convert it to absolute.
|
// If the CLI was provided a directory, convert it to absolute.
|
||||||
Some(path) => std::path::absolute(path)?,
|
Some(path) => std::path::absolute(path)?,
|
||||||
// If no path was provided, use current directory.
|
// If no path was provided, use the current directory.
|
||||||
None => std::env::current_dir().unwrap_or_else(|_|
|
None => std::env::current_dir().unwrap_or_else(|_|
|
||||||
// If we can't find the CWD attempt to open the home directory.
|
// If we can't find the CWD, attempt to open the home directory.
|
||||||
dirs::home_dir().expect("Failed to access filesystem.")),
|
dirs::home_dir().expect("Failed to access filesystem.")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
39
src/tui.rs
39
src/tui.rs
@ -1,42 +1,13 @@
|
|||||||
|
pub mod app;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use ratatui::{DefaultTerminal, Frame};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::time::Duration;
|
|
||||||
use ratatui::crossterm::event;
|
|
||||||
use ratatui::crossterm::event::{Event, KeyCode};
|
|
||||||
use ratatui::widgets::Paragraph;
|
|
||||||
|
|
||||||
pub fn start(root_path: std::path::PathBuf) -> Result<()> {
|
pub fn start(root_path: std::path::PathBuf) -> Result<()> {
|
||||||
println!("Starting the TUI editor at {:?}", root_path);
|
println!("Starting the TUI editor at {:?}", root_path);
|
||||||
let terminal = ratatui::init();
|
let terminal = ratatui::init();
|
||||||
let app_result = run(terminal, root_path).context("Failed to start the TUI editor.");
|
let app_result = app::App::new(&root_path)
|
||||||
|
.run(terminal)
|
||||||
|
.context("Failed to start the TUI editor.");
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
app_result
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(
|
|
||||||
mut terminal: DefaultTerminal,
|
|
||||||
root_path: std::path::PathBuf,
|
|
||||||
) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
terminal.draw(draw)?;
|
|
||||||
if should_quit()? {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn should_quit() -> Result<bool> {
|
|
||||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
|
||||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
|
||||||
return Ok(KeyCode::Char('q') == key.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw(frame: &mut Frame) {
|
|
||||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
|
||||||
frame.render_widget(greeting, frame.area());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
146
src/tui/app.rs
Normal file
146
src/tui/app.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::crossterm::event;
|
||||||
|
use ratatui::crossterm::event::{Event, KeyCode};
|
||||||
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
|
use ratatui::prelude::{Color, Style, Widget};
|
||||||
|
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap};
|
||||||
|
use ratatui::{DefaultTerminal, symbols};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct App<'a> {
|
||||||
|
root_path: &'a std::path::Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> App<'a> {
|
||||||
|
pub(crate) fn new(root_path: &'a std::path::Path) -> Self {
|
||||||
|
Self { root_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| f.render_widget(self, f.area()))?;
|
||||||
|
if self.should_quit()? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self) -> anyhow::Result<()> {
|
||||||
|
// Handle other keyboard events here, aside from quitting.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_quit(self) -> anyhow::Result<bool> {
|
||||||
|
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||||
|
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||||
|
return Ok(KeyCode::Char('q') == key.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_status(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Tabs::new(["File", "Edit", "View", "Help"])
|
||||||
|
.style(Style::default())
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tabs(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// TODO: Tabs should be opened from file explorer
|
||||||
|
Tabs::new(["file.md", "file.cpp"])
|
||||||
|
.divider(symbols::DOT)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::NONE)
|
||||||
|
.padding(Padding::new(0, 0, 0, 0)),
|
||||||
|
)
|
||||||
|
.highlight_style(Style::default().fg(Color::LightRed))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_editor(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// TODO: Title should be detected programming language name
|
||||||
|
// TODO: Content should be file contents
|
||||||
|
// TODO: Contents should use vim in rendered TTY
|
||||||
|
// TODO: Vimrc should be used
|
||||||
|
Paragraph::new("This is an example of the TUI interface (press 'q' to quit)")
|
||||||
|
.style(Style::default())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Rust")
|
||||||
|
.title_style(Style::default().fg(Color::Yellow))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.padding(Padding::new(0, 0, 0, 1)),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_terminal(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// TODO: Title should be detected shell name
|
||||||
|
// TODO: Contents should be shell output
|
||||||
|
Paragraph::new("Terminal placeholder")
|
||||||
|
.style(Style::default())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Bash")
|
||||||
|
.title_style(Style::default().fg(Color::DarkGray))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_file_explorer(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("File explorer placeholder")
|
||||||
|
.style(Style::default())
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Separate complex components into their own widgets.
|
||||||
|
impl<'a> Widget for App<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let vertical = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3), // status bar
|
||||||
|
Constraint::Percentage(70), // horizontal layout
|
||||||
|
Constraint::Percentage(30), // terminal
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let horizontal = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Max(30), // File explorer with a max width of 30 characters.
|
||||||
|
Constraint::Fill(1), // Editor fills the remaining space.
|
||||||
|
])
|
||||||
|
.split(vertical[1]);
|
||||||
|
|
||||||
|
let editor_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1), // Editor tabs.
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.split(horizontal[1]);
|
||||||
|
|
||||||
|
self.draw_status(vertical[0], buf);
|
||||||
|
self.draw_terminal(vertical[2], buf);
|
||||||
|
|
||||||
|
self.draw_file_explorer(horizontal[0], buf);
|
||||||
|
|
||||||
|
self.draw_tabs(editor_layout[0], buf);
|
||||||
|
self.draw_editor(editor_layout[1], buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user