use revue::prelude::*;
use revue::widget::{Command, CommandPalette, TextArea};
struct IdeApp {
command_palette_open: bool,
command_palette: CommandPalette,
current_file: String,
files: Vec<FileItem>,
selected_file: usize,
editor: TextArea,
status_message: String,
mode: EditorMode,
split_ratio: f32,
notifications: Vec<String>,
}
#[derive(Clone, Copy, PartialEq)]
enum EditorMode {
Normal,
Insert,
Command,
}
impl EditorMode {
fn name(&self) -> &str {
match self {
EditorMode::Normal => "NORMAL",
EditorMode::Insert => "INSERT",
EditorMode::Command => "COMMAND",
}
}
fn color(&self) -> Color {
match self {
EditorMode::Normal => Color::BLUE,
EditorMode::Insert => Color::GREEN,
EditorMode::Command => Color::YELLOW,
}
}
}
struct FileItem {
name: String,
is_dir: bool,
modified: bool,
}
impl IdeApp {
fn new() -> Self {
let commands = vec![
Command::new("file.new", "New File").shortcut("Ctrl+N"),
Command::new("file.open", "Open File").shortcut("Ctrl+O"),
Command::new("file.save", "Save File").shortcut("Ctrl+S"),
Command::new("file.close", "Close File").shortcut("Ctrl+W"),
Command::new("edit.undo", "Undo").shortcut("Ctrl+Z"),
Command::new("edit.redo", "Redo").shortcut("Ctrl+Y"),
Command::new("edit.find", "Find").shortcut("Ctrl+F"),
Command::new("edit.replace", "Replace").shortcut("Ctrl+H"),
Command::new("view.sidebar", "Toggle Sidebar").shortcut("Ctrl+B"),
Command::new("view.terminal", "Toggle Terminal").shortcut("Ctrl+`"),
Command::new("goto.line", "Go to Line").shortcut("Ctrl+G"),
Command::new("goto.symbol", "Go to Symbol").shortcut("Ctrl+Shift+O"),
];
let files = vec![
FileItem {
name: "src/".into(),
is_dir: true,
modified: false,
},
FileItem {
name: " main.rs".into(),
is_dir: false,
modified: true,
},
FileItem {
name: " lib.rs".into(),
is_dir: false,
modified: false,
},
FileItem {
name: " app/".into(),
is_dir: true,
modified: false,
},
FileItem {
name: " mod.rs".into(),
is_dir: false,
modified: false,
},
FileItem {
name: " screen.rs".into(),
is_dir: false,
modified: true,
},
FileItem {
name: "Cargo.toml".into(),
is_dir: false,
modified: false,
},
FileItem {
name: "README.md".into(),
is_dir: false,
modified: false,
},
];
let sample_code = r#"//! Revue - A Vue-style TUI framework for Rust
//!
//! This is a sample file showing syntax highlighting.
use revue::prelude::*;
fn main() -> Result<()> {
let mut app = App::builder()
.style("styles.css")
.hot_reload(true)
.build();
let view = Text::new("Hello, Revue!")
.fg(Color::CYAN)
.bold();
app.run(&view)
}
// TODO: Add more features
// FIXME: Handle edge cases
"#;
let mut editor = TextArea::new();
editor.set_content(sample_code);
Self {
command_palette_open: false,
command_palette: CommandPalette::new().commands(commands),
current_file: "main.rs".into(),
files,
selected_file: 1,
editor,
status_message: "Ready".into(),
mode: EditorMode::Normal,
split_ratio: 0.2,
notifications: Vec::new(),
}
}
fn handle_key(&mut self, key: &Key) -> bool {
if self.command_palette_open {
match key {
Key::Escape => {
self.command_palette_open = false;
self.command_palette.clear_query();
return true;
}
Key::Enter => {
if let Some(cmd) = self.command_palette.selected_command() {
self.execute_command(&cmd.id.clone());
}
self.command_palette_open = false;
self.command_palette.clear_query();
return true;
}
Key::Up => {
self.command_palette.select_prev();
return true;
}
Key::Down => {
self.command_palette.select_next();
return true;
}
Key::Char(c) => {
self.command_palette.input(*c);
return true;
}
Key::Backspace => {
self.command_palette.backspace();
return true;
}
_ => return false,
}
}
match self.mode {
EditorMode::Normal => self.handle_normal_mode(key),
EditorMode::Insert => self.handle_insert_mode(key),
EditorMode::Command => self.handle_command_mode(key),
}
}
fn handle_normal_mode(&mut self, key: &Key) -> bool {
match key {
Key::Char('i') => {
self.mode = EditorMode::Insert;
self.status_message = "-- INSERT --".into();
true
}
Key::Char(':') => {
self.mode = EditorMode::Command;
self.status_message = ":".into();
true
}
Key::Char('j') | Key::Down => {
self.editor.move_down();
true
}
Key::Char('k') | Key::Up => {
self.editor.move_up();
true
}
Key::Char('h') | Key::Left => {
self.editor.move_left();
true
}
Key::Char('l') | Key::Right => {
self.editor.move_right();
true
}
Key::Char('p') if key == &Key::Char('p') => {
self.command_palette_open = true;
true
}
Key::Tab => {
self.selected_file = (self.selected_file + 1) % self.files.len();
true
}
_ => false,
}
}
fn handle_insert_mode(&mut self, key: &Key) -> bool {
match key {
Key::Escape => {
self.mode = EditorMode::Normal;
self.status_message = "Ready".into();
true
}
Key::Char(c) => {
self.editor.insert_char(*c);
true
}
Key::Enter => {
self.editor.insert_char('\n');
true
}
Key::Backspace => {
self.editor.delete_char_before();
true
}
Key::Delete => {
self.editor.delete_char_at();
true
}
Key::Up => {
self.editor.move_up();
true
}
Key::Down => {
self.editor.move_down();
true
}
Key::Left => {
self.editor.move_left();
true
}
Key::Right => {
self.editor.move_right();
true
}
_ => false,
}
}
fn handle_command_mode(&mut self, key: &Key) -> bool {
match key {
Key::Escape => {
self.mode = EditorMode::Normal;
self.status_message = "Ready".into();
true
}
Key::Enter => {
let cmd = self.status_message.trim_start_matches(':').to_string();
self.execute_vim_command(&cmd);
self.mode = EditorMode::Normal;
true
}
Key::Char(c) => {
self.status_message.push(*c);
true
}
Key::Backspace => {
if self.status_message.len() > 1 {
self.status_message.pop();
}
true
}
_ => false,
}
}
fn execute_command(&mut self, id: &str) {
self.status_message = format!("Executed: {}", id);
self.add_notification(format!("Command: {}", id));
match id {
"file.new" => self.status_message = "New file created".into(),
"file.save" => self.status_message = format!("Saved: {}", self.current_file),
"view.sidebar" => self.split_ratio = if self.split_ratio > 0.1 { 0.0 } else { 0.2 },
_ => {}
}
}
fn execute_vim_command(&mut self, cmd: &str) {
match cmd {
"w" => self.status_message = format!("\"{}\" written", self.current_file),
"q" => self.status_message = "Use Ctrl+C to quit".into(),
"wq" => self.status_message = "Saved and quit".into(),
_ => self.status_message = format!("Unknown command: {}", cmd),
}
}
fn add_notification(&mut self, msg: String) {
self.notifications.push(msg);
if self.notifications.len() > 3 {
self.notifications.remove(0);
}
}
fn render_file_tree(&self) -> impl View {
let mut tree = vstack();
for (i, file) in self.files.iter().enumerate() {
let icon = if file.is_dir { "📁 " } else { "📄 " };
let modified = if file.modified { " [+]" } else { "" };
let name = format!("{}{}{}", icon, file.name, modified);
let text = if i == self.selected_file {
Text::new(name).fg(Color::CYAN).bold()
} else if file.is_dir {
Text::new(name).fg(Color::BLUE)
} else if file.modified {
Text::new(name).fg(Color::YELLOW)
} else {
Text::new(name)
};
tree = tree.child(text);
}
Border::rounded().title("Explorer").child(tree)
}
fn render_editor(&self) -> impl View {
let text = self.editor.get_content();
let lines = text.lines().count().max(1);
let line_width = lines.to_string().len();
let mut content = vstack();
for (i, line) in text.lines().enumerate() {
let line_num = format!("{:>width$} ", i + 1, width = line_width);
let row = hstack()
.child(Text::new(line_num).fg(Color::rgb(100, 100, 100)))
.child(Text::new(line));
content = content.child(row);
}
let (cursor_row, cursor_col) = self.editor.cursor_position();
let cursor_info = format!("Ln {}, Col {}", cursor_row + 1, cursor_col + 1);
let header = hstack()
.child(Text::new(format!(" {} ", self.current_file)).bg(Color::rgb(50, 50, 50)))
.child(if self.files[self.selected_file].modified {
Text::new(" [Modified]").fg(Color::YELLOW)
} else {
Text::new("")
});
vstack()
.child(header)
.child(Border::single().child(content))
.child(Text::new(cursor_info).fg(Color::rgb(128, 128, 128)))
}
fn render_status_bar(&self) -> impl View {
let mode_text = Text::new(format!(" {} ", self.mode.name()))
.fg(Color::BLACK)
.bg(self.mode.color())
.bold();
let file_text = Text::new(format!(" {} ", self.current_file));
let status_text =
Text::new(format!(" {} ", self.status_message)).fg(Color::rgb(180, 180, 180));
let (cursor_row, cursor_col) = self.editor.cursor_position();
let pos_text = Text::new(format!(" {}:{} ", cursor_row + 1, cursor_col + 1));
hstack()
.child(mode_text)
.child(file_text)
.child(status_text)
.child(pos_text)
}
fn render_command_palette(&self) -> impl View {
if !self.command_palette_open {
return vstack(); }
let search_box = Border::rounded().title("Command Palette").child(
vstack()
.child(Text::new(format!("> {}", self.command_palette.get_query())))
.child(Text::new("─".repeat(40)).fg(Color::rgb(80, 80, 80)))
.child(
Text::new("(Use ↑↓ to select, Enter to execute)").fg(Color::rgb(100, 100, 100)),
),
);
vstack().child(search_box)
}
}
impl View for IdeApp {
fn render(&self, ctx: &mut RenderContext) {
let sidebar = self.render_file_tree();
let editor = self.render_editor();
let main_content = if self.split_ratio > 0.05 {
hstack().child(sidebar).child(editor)
} else {
hstack().child(editor)
};
let header = hstack()
.child(Text::new(" Revue IDE ").fg(Color::CYAN).bold())
.child(
Text::new(" | Ctrl+P: Commands | Tab: Files | i: Insert | :: Command ")
.fg(Color::rgb(100, 100, 100)),
);
let main_view = vstack()
.child(header)
.child(main_content)
.child(self.render_status_bar());
main_view.render(ctx);
if self.command_palette_open {
self.render_command_palette().render(ctx);
}
if !self.notifications.is_empty() {
let mut notif_stack = vstack();
for msg in &self.notifications {
notif_stack = notif_stack.child(
Text::new(format!(" {} ", msg))
.fg(Color::WHITE)
.bg(Color::rgb(60, 60, 60)),
);
}
notif_stack.render(ctx);
}
}
}
fn main() -> Result<()> {
let mut app = App::builder().build();
let ide = IdeApp::new();
app.run_with_handler(ide, |key_event, ide| ide.handle_key(&key_event.key))
}