48 Commits

Author SHA1 Message Date
3450d6db9b WIP 2026-02-21 21:10:12 -05:00
8ce92b435b Update CI badge. 2026-02-21 20:12:04 -05:00
bc29502ad4 Add workspace dependencies. 2026-02-21 20:08:41 -05:00
4622102d81 Renames. 2026-02-21 19:45:30 -05:00
dc680554dd Set up workspace, add formatting CI. 2026-02-21 19:16:50 -05:00
177a4bc432 Fix clide lints. 2026-02-21 19:03:18 -05:00
5f4391cb82 Fix libclide lints. 2026-02-21 18:38:07 -05:00
2273c0156e Clean up CI. 2026-02-21 18:30:26 -05:00
417f01b527 remove [env] 2026-02-21 18:24:18 -05:00
d776602fe8 Remove debug. 2026-02-21 18:20:51 -05:00
e2ddadd952 Don't force in cargo.toml 2026-02-21 18:20:29 -05:00
61c7f59237 debug 2026-02-21 18:16:56 -05:00
1e63eabd46 Set qmake? 2026-02-21 18:12:45 -05:00
ac83b3e30f Don't set QMAKE. 2026-02-21 18:04:53 -05:00
a42ad73a57 Add setup action. 2026-02-21 18:01:51 -05:00
1ec13aa43a Move env. 2026-02-21 17:52:38 -05:00
0a3f095080 Install qt using action. 2026-02-21 17:51:09 -05:00
6474c5b6bd Reuse steps. 2026-02-21 17:49:42 -05:00
717ea70895 Fix qmake. 2026-02-21 17:37:46 -05:00
8eada4bbee Update push. 2026-02-21 17:35:04 -05:00
0ebd45ae15 Move CI to github.
Some checks failed
Build / Build (pull_request) Failing after 9s
Why not, it's free.
2026-02-21 17:32:44 -05:00
14e7514cc1 Merge jobs.
Some checks failed
Build / Build (pull_request) Failing after 31m19s
2026-02-21 17:25:58 -05:00
b3bb13fa33 22.04
Some checks failed
Build / Build (pull_request) Failing after 10s
Build / Test (pull_request) Failing after 4s
2026-02-21 17:24:36 -05:00
ad95056376 fix path.
Some checks failed
Build / Build (pull_request) Failing after 4s
Build / Test (pull_request) Failing after 4s
2026-02-21 17:23:22 -05:00
384fa51b6e Test with ubuntu:latest.
Some checks failed
Build / Build (pull_request) Failing after 6s
Build / Test (pull_request) Failing after 3s
ubuntu:24.04 was not defined in the runner's docker-compose.yaml server side.
2026-02-21 17:21:40 -05:00
fc2a44740f Use ubuntu-latest.
Some checks failed
Build / Build (pull_request) Failing after 4m36s
Build / Test (pull_request) Failing after 3s
2026-02-21 17:17:52 -05:00
f609aa02db Update env.
Some checks failed
Build / Build (pull_request) Failing after 5s
Build / Test (pull_request) Failing after 5s
2026-02-21 17:12:07 -05:00
fdb4f0db0b Test named volume.
Some checks failed
Build / Build (pull_request) Failing after 10s
Build / Test (pull_request) Failing after 4s
2026-02-21 16:53:03 -05:00
2d5e721a79 Update environment.
Some checks failed
Build / Build (pull_request) Failing after 7s
2026-02-21 16:52:42 -05:00
2e55ba1a4b Source rust things.
Some checks failed
Build / Build (pull_request) Failing after 19m32s
2026-02-21 16:40:34 -05:00
6b9e3b1b40 Update name. 2026-02-21 16:26:18 -05:00
bdb126cab5 Fix apt install.
Some checks failed
Build / Build (pull_request) Failing after 17m52s
2026-02-21 16:22:10 -05:00
a4f6f199ec Test.
Some checks failed
Build / Build (pull_request) Failing after 50s
2026-02-21 16:16:19 -05:00
911a29937e Install libgl. 2026-02-21 16:16:06 -05:00
579826d398 Add badge for build CI. 2026-02-21 16:00:59 -05:00
3b1f33f055 Fix qt version.
Some checks failed
Build / Build (pull_request) Failing after 17m17s
2026-02-21 15:54:40 -05:00
607dae32fe Checkout.
Some checks failed
Build / Build (pull_request) Failing after 3m56s
2026-02-21 15:49:16 -05:00
bb032e9daf Source rust.
Some checks failed
Build / Build (pull_request) Failing after 2m0s
2026-02-21 15:45:43 -05:00
886a32a9e2 Set QMAKE.
Some checks failed
Build / Build (pull_request) Failing after 1m55s
2026-02-21 15:42:55 -05:00
0c58b6c436 Update arch.
Some checks failed
Build / Build (pull_request) Failing after 1m59s
2026-02-21 15:38:05 -05:00
df3547267b update
Some checks failed
Build / Build (pull_request) Failing after 1m31s
2026-02-21 15:34:30 -05:00
a605c4929e Use aqtinstall directly.
Some checks failed
Build / Build (pull_request) Failing after 40s
2026-02-21 15:33:11 -05:00
a40125416d Python
Some checks failed
Build / Build (pull_request) Failing after 57s
2026-02-21 15:28:00 -05:00
6777a44b3b Remove sudo
Some checks failed
Build / Build (pull_request) Failing after 1m9s
2026-02-21 15:25:01 -05:00
288298ac18 Install python.
Some checks failed
Build / Build (pull_request) Failing after 49s
2026-02-21 15:21:53 -05:00
d461a29ff9 Yes.
Some checks failed
Build / Build (push) Failing after 54s
Build / Build (pull_request) Failing after 56s
2026-02-21 15:18:42 -05:00
bc906cd7f3 Install things in CI.
Some checks failed
Build / Build (push) Failing after 1m26s
Build / Build (pull_request) Failing after 16s
2026-02-21 15:15:18 -05:00
8ddff3fe9e Test gitea CI.
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 23s
2026-02-21 15:06:08 -05:00
21 changed files with 274 additions and 105 deletions

View File

@@ -1,6 +1,2 @@
[build] [build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ] rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"

20
.github/actions/setup-qt/action.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: "Setup Qt"
description: "Install clide dependencies"
inputs:
qt-version:
description: "Qt version to install"
required: true
runs:
using: "composite"
steps:
- name: Install apt packages
run: |
sudo apt update -y
sudo apt install -y build-essential cmake curl libgl1-mesa-dev python3 python3-pip
shell: bash
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.qt-version }}

85
.github/workflows/check.yaml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Check
on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:
env:
QT_VERSION: 6.7.3
jobs:
Build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Build libclide
run: |
cargo b -p libclide --release
- name: Build clide
run: |
cargo b --release
Test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Test libclide
run: |
cargo test -p libclide
- name: Test clide
run: |
cargo test
Lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Lint libclide
run: |
cargo clippy -p libclide -- -D warnings
- name: Lint clide
run: |
cargo clippy -- -D warnings
Format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Format libclide
run: |
cargo fmt -p libclide --verbose -- --check
- name: Format clide
run: |
cargo fmt --verbose -- --check

1
Cargo.lock generated
View File

@@ -1169,6 +1169,7 @@ name = "libclide"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"strum",
] ]
[[package]] [[package]]

View File

@@ -3,6 +3,17 @@ name = "clide"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[workspace]
resolver = "3"
members = [
".",
"libclide",
]
[workspace.dependencies]
anyhow = "1.0.100"
strum = "0.27.2"
[dependencies] [dependencies]
cxx = "1.0.95" cxx = "1.0.95"
cxx-qt = "0.8.0" cxx-qt = "0.8.0"
@@ -12,13 +23,13 @@ dirs = "6.0.0"
syntect = "5.2.0" syntect = "5.2.0"
clap = { version = "4.5.54", features = ["derive"] } clap = { version = "4.5.54", features = ["derive"] }
ratatui = "0.30.0" ratatui = "0.30.0"
anyhow = "1.0.100"
tui-tree-widget = "0.24.0" tui-tree-widget = "0.24.0"
tui-logger = "0.18.1" tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
strum = "0.27.2"
devicons = "0.6.12" devicons = "0.6.12"
libclide = { path = "./libclide" } libclide = { path = "./libclide" }
anyhow = { workspace = true }
strum = { workspace = true }
[build-dependencies] [build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6. # The link_qt_object_files feature is required for statically linking Qt 6.

View File

@@ -1,5 +1,7 @@
# CLIDE # CLIDE
[![Check](https://github.com/shaunrd0/clide/actions/workflows/check.yaml/badge.svg)](https://github.com/shaunrd0/clide/actions/workflows/check.yaml)
CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments.
The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.

View File

@@ -1,7 +1,7 @@
use cxx_qt_build::{CxxQtBuilder, QmlModule}; use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() { fn main() {
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[ CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files([
"qml/ClideApplicationView.qml", "qml/ClideApplicationView.qml",
"qml/ClideEditorView.qml", "qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml", "qml/ClideExplorerView.qml",

View File

@@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.102" anyhow = { workspace = true }
strum = { workspace = true }

View File

@@ -2,8 +2,8 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug)] #[derive(Debug)]
pub struct EntryMeta { pub struct EntryMeta {
@@ -35,7 +35,7 @@ impl EntryMeta {
pub fn new<P: AsRef<Path>>(p: P) -> Result<Self> { pub fn new<P: AsRef<Path>>(p: P) -> Result<Self> {
let path = p.as_ref(); let path = p.as_ref();
let is_dir = path.is_dir(); let is_dir = path.is_dir();
let abs_path = Self::normalize(&path).to_string_lossy().to_string(); let abs_path = Self::normalize(path).to_string_lossy().to_string();
let file_name = Path::new(&abs_path) let file_name = Path::new(&abs_path)
.file_name() .file_name()
.context(format!("Failed to get file name for path: {abs_path:?}"))? .context(format!("Failed to get file name for path: {abs_path:?}"))?

View File

@@ -3,3 +3,4 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod fs; pub mod fs;
pub mod theme;

5
libclide/src/theme.rs Normal file
View File

@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod colors;

View File

@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub struct Colors {}
impl Colors {
pub const HOVERED: &str = "#303234";
pub const UNHOVERED: &str = "#3c3f41";
pub const PRESSED: &str = "#4b4f51";
pub const MENUBAR: &str = "#262626";
pub const MENUBAR_BORDER: &str = "#575757";
pub const SCROLLBAR: &str = "#4b4f51";
pub const SCROLLBAR_ACTIVE: &str = "#4b4f51";
pub const SCROLLBAR_GUTTER: &str = "#3b3b3b";
pub const LINENUMBER: &str = "#94989b";
pub const ACTIVE: &str = "#a9acb0";
pub const INACTIVE: &str = "#FFF";
pub const EDITOR_BACKGROUND: &str = "#1E1F22";
pub const EDITOR_TEXT: &str = "#acaea3";
pub const EDITOR_HIGHLIGHTED_TEXT: &str = "#ccced3";
pub const EDITOR_HIGHLIGHT: &str = "#ccced3";
pub const GUTTER: &str = "#1e1f22";
pub const EXPLORER_HOVERED: &str = "#4c5053";
pub const EXPLORER_TEXT: &str = "#FFF";
pub const EXPLORER_TEXT_SELECTED: &str = "#262626";
pub const EXPLORER_BACKGROUND: &str = "#1E1F22";
pub const EXPLORER_FOLDER: &str = "#54585b";
pub const EXPLORER_FOLDER_OPEN: &str = "#393B40";
pub const TERMINAL_BACKGROUND: &str = "#111111";
pub const INFO_LOG: &str = "#C4FFFF";
pub const DEBUG_LOG: &str = "#9148AF";
pub const WARN_LOG: &str = "#C4A958";
pub const ERROR_LOG: &str = "#ff5555";
pub const TRACE_LOG: &str = "#ffaa00";
pub fn css_to_u32(css: &str) -> Result<u32, String> {
let hex = css.trim_start_matches('#');
// Expand shorthand #RGB to #RRGGBB
let hex_full = match hex.len() {
3 => hex.chars()
.map(|c| std::iter::repeat(c).take(2).collect::<String>())
.collect::<String>(),
6 => hex.to_string(),
_ => return Err(format!("Invalid hex color length: {}", hex)),
};
// Parse the hex string as u32, masking to ensure the top byte is 0x00.
u32::from_str_radix(&hex_full, 16)
.map(|rgb| rgb & 0x00FF_FFFF)
.map_err(|e| format!("Failed to parse hex: {}", e))
}
}

View File

@@ -2,8 +2,10 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
#[cxx_qt::bridge] use cxx_qt_lib::QColor;
use libclide::theme::colors::Colors;
#[cxx_qt::bridge]
pub mod qobject { pub mod qobject {
unsafe extern "C++" { unsafe extern "C++" {
include!("cxx-qt-lib/qcolor.h"); include!("cxx-qt-lib/qcolor.h");
@@ -46,8 +48,6 @@ pub mod qobject {
} }
} }
use cxx_qt_lib::QColor;
pub struct RustColorsImpl { pub struct RustColorsImpl {
hovered: QColor, hovered: QColor,
unhovered: QColor, unhovered: QColor,
@@ -82,34 +82,34 @@ pub struct RustColorsImpl {
impl Default for RustColorsImpl { impl Default for RustColorsImpl {
fn default() -> Self { fn default() -> Self {
Self { Self {
hovered: QColor::try_from("#303234").unwrap(), hovered: QColor::try_from(Colors::HOVERED).unwrap(),
unhovered: QColor::try_from("#3c3f41").unwrap(), unhovered: QColor::try_from(Colors::UNHOVERED).unwrap(),
pressed: QColor::try_from("#4b4f51").unwrap(), pressed: QColor::try_from(Colors::PRESSED).unwrap(),
menubar: QColor::try_from("#262626").unwrap(), menubar: QColor::try_from(Colors::MENUBAR).unwrap(),
menubar_border: QColor::try_from("#575757").unwrap(), menubar_border: QColor::try_from(Colors::MENUBAR_BORDER).unwrap(),
scrollbar: QColor::try_from("#4b4f51").unwrap(), scrollbar: QColor::try_from(Colors::SCROLLBAR).unwrap(),
scrollbar_active: QColor::try_from("#4b4f51").unwrap(), scrollbar_active: QColor::try_from(Colors::SCROLLBAR_ACTIVE).unwrap(),
scrollbar_gutter: QColor::try_from("#3b3b3b").unwrap(), scrollbar_gutter: QColor::try_from(Colors::SCROLLBAR_GUTTER).unwrap(),
linenumber: QColor::try_from("#94989b").unwrap(), linenumber: QColor::try_from(Colors::LINENUMBER).unwrap(),
active: QColor::try_from("#a9acb0").unwrap(), active: QColor::try_from(Colors::ACTIVE).unwrap(),
inactive: QColor::try_from("#FFF").unwrap(), inactive: QColor::try_from(Colors::INACTIVE).unwrap(),
editor_background: QColor::try_from("#1E1F22").unwrap(), editor_background: QColor::try_from(Colors::EDITOR_BACKGROUND).unwrap(),
editor_text: QColor::try_from("#acaea3").unwrap(), editor_text: QColor::try_from(Colors::EDITOR_TEXT).unwrap(),
editor_highlighted_text: QColor::try_from("#ccced3").unwrap(), editor_highlighted_text: QColor::try_from(Colors::EDITOR_HIGHLIGHTED_TEXT).unwrap(),
editor_highlight: QColor::try_from("#ccced3").unwrap(), editor_highlight: QColor::try_from(Colors::EDITOR_HIGHLIGHT).unwrap(),
gutter: QColor::try_from("#1e1f22").unwrap(), gutter: QColor::try_from(Colors::GUTTER).unwrap(),
explorer_hovered: QColor::try_from("#4c5053").unwrap(), explorer_hovered: QColor::try_from(Colors::EXPLORER_HOVERED).unwrap(),
explorer_text: QColor::try_from("#FFF").unwrap(), explorer_text: QColor::try_from(Colors::EXPLORER_TEXT).unwrap(),
explorer_text_selected: QColor::try_from("#262626").unwrap(), explorer_text_selected: QColor::try_from(Colors::EXPLORER_TEXT_SELECTED).unwrap(),
explorer_background: QColor::try_from("#1E1F22").unwrap(), explorer_background: QColor::try_from(Colors::EXPLORER_BACKGROUND).unwrap(),
explorer_folder: QColor::try_from("#54585b").unwrap(), explorer_folder: QColor::try_from(Colors::EXPLORER_FOLDER).unwrap(),
explorer_folder_open: QColor::try_from("#393B40").unwrap(), explorer_folder_open: QColor::try_from(Colors::EXPLORER_FOLDER_OPEN).unwrap(),
terminal_background: QColor::try_from("#111111").unwrap(), terminal_background: QColor::try_from(Colors::TERMINAL_BACKGROUND).unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(), info_log: QColor::try_from(Colors::INFO_LOG).unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(), debug_log: QColor::try_from(Colors::DEBUG_LOG).unwrap(),
warn_log: QColor::try_from("#C4A958").unwrap(), warn_log: QColor::try_from(Colors::WARN_LOG).unwrap(),
error_log: QColor::try_from("#ff5555").unwrap(), error_log: QColor::try_from(Colors::ERROR_LOG).unwrap(),
trace_log: QColor::try_from("#ffaa00").unwrap(), trace_log: QColor::try_from(Colors::TRACE_LOG).unwrap(),
} }
} }
} }

View File

@@ -76,7 +76,7 @@ impl qobject::FileSystem {
return QString::default(); return QString::default();
} }
let meta = fs::metadata(path.to_string()) let meta = fs::metadata(path.to_string())
.expect(format!("Failed to get file metadata {path:?}").as_str()); .unwrap_or_else(|_| panic!("Failed to get file metadata {path:?}"));
if !meta.is_file() { if !meta.is_file() {
warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file"); warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
return QString::default(); return QString::default();
@@ -114,7 +114,7 @@ impl qobject::FileSystem {
output.push_str("</pre>\n"); output.push_str("</pre>\n");
QString::from(output) QString::from(output)
} else { } else {
return QString::default(); QString::default()
} }
} }
@@ -126,7 +126,7 @@ impl qobject::FileSystem {
fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex { fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex {
if !path.is_empty() if !path.is_empty()
&& fs::metadata(path.to_string()) && fs::metadata(path.to_string())
.expect(format!("Failed to get metadata for path {path:?}").as_str()) .unwrap_or_else(|_| panic!("Failed to get metadata for path {path:?}"))
.is_dir() .is_dir()
{ {
self.set_root_path(path) self.set_root_path(path)
@@ -147,7 +147,7 @@ impl qobject::FileSystem {
if Path::new(&str).is_dir() { if Path::new(&str).is_dir() {
// Ensures directories are given a folder icon and not mistakenly resolved to a language. // Ensures directories are given a folder icon and not mistakenly resolved to a language.
// For example, a directory named `cpp` would otherwise return a C++ icon. // For example, a directory named `cpp` would otherwise return a C++ icon.
return QString::from(FileIcon::from("dir/").to_string()) return QString::from(FileIcon::from("dir/").to_string());
} }
let icon = FileIcon::from(str); let icon = FileIcon::from(str);
QString::from(icon.to_string()) QString::from(icon.to_string())

View File

@@ -82,7 +82,7 @@ fn main() -> Result<()> {
RunMode::Gui => { RunMode::Gui => {
trace!(target:"main()", "Starting GUI in a new process"); trace!(target:"main()", "Starting GUI in a new process");
Command::new(std::env::current_exe()?) Command::new(std::env::current_exe()?)
.args(&["--gui", app_context.path.to_str().unwrap()]) .args(["--gui", app_context.path.to_str().unwrap()])
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.stdin(Stdio::null()) .stdin(Stdio::null())

View File

@@ -68,8 +68,8 @@ impl Widget for About {
.map(|l| Line::from(Span::raw(*l))) .map(|l| Line::from(Span::raw(*l)))
.collect(); .collect();
Clear::default().render(kilroy_rect, buf); Clear.render(kilroy_rect, buf);
Clear::default().render(chunks[1], buf); Clear.render(chunks[1], buf);
Paragraph::new(about_lines) Paragraph::new(about_lines)
.block( .block(
Block::default() Block::default()

View File

@@ -3,13 +3,11 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::about::About; use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState}; use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};
use crate::tui::editor_tab::EditorTab; use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
use crate::tui::menu_bar::MenuBar; use crate::tui::menu_bar::MenuBar;
use AppComponent::AppMenuBar;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::{error, info, trace}; use log::{error, info, trace};
use ratatui::DefaultTerminal; use ratatui::DefaultTerminal;
@@ -26,10 +24,10 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppComponent { pub enum AppComponent {
AppEditor, Editor,
AppExplorer, Explorer,
AppLogger, Logger,
AppMenuBar, MenuBar,
} }
pub struct App<'a> { pub struct App<'a> {
@@ -51,7 +49,7 @@ impl<'a> App<'a> {
explorer: Explorer::new(&root_path)?, explorer: Explorer::new(&root_path)?,
logger: Logger::new(), logger: Logger::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
last_active: AppEditor, last_active: AppComponent::Editor,
about: false, about: false,
}; };
Ok(app) Ok(app)
@@ -87,7 +85,7 @@ impl<'a> App<'a> {
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
// Determine help text from the most recently focused component. // Determine help text from the most recently focused component.
let help = match self.last_active { let help = match self.last_active {
AppEditor => match self.editor_tab.current_editor() { AppComponent::Editor => match self.editor_tab.current_editor() {
Some(editor) => editor.component_state.help_text.clone(), Some(editor) => editor.component_state.help_text.clone(),
None => { None => {
if !self.editor_tab.is_empty() { if !self.editor_tab.is_empty() {
@@ -96,9 +94,9 @@ impl<'a> App<'a> {
"Failed to get current Editor while getting widget help text".to_string() "Failed to get current Editor while getting widget help text".to_string()
} }
}, },
AppExplorer => self.explorer.component_state.help_text.clone(), AppComponent::Explorer => self.explorer.component_state.help_text.clone(),
AppLogger => self.logger.component_state.help_text.clone(), AppComponent::Logger => self.logger.component_state.help_text.clone(),
AppMenuBar => self.menu_bar.component_state.help_text.clone(), AppComponent::MenuBar => self.menu_bar.component_state.help_text.clone(),
}; };
Paragraph::new( Paragraph::new(
concat!( concat!(
@@ -132,15 +130,15 @@ impl<'a> App<'a> {
info!(target:Self::ID, "Changing widget focus to {:?}", focus); info!(target:Self::ID, "Changing widget focus to {:?}", focus);
self.clear_focus(); self.clear_focus();
match focus { match focus {
AppEditor => match self.editor_tab.current_editor_mut() { AppComponent::Editor => match self.editor_tab.current_editor_mut() {
None => { None => {
error!(target:Self::ID, "Failed to get current Editor while changing focus") error!(target:Self::ID, "Failed to get current Editor while changing focus")
} }
Some(editor) => editor.component_state.set_focus(Focus::Active), Some(editor) => editor.component_state.set_focus(Focus::Active),
}, },
AppExplorer => self.explorer.component_state.set_focus(Focus::Active), AppComponent::Explorer => self.explorer.component_state.set_focus(Focus::Active),
AppLogger => self.logger.component_state.set_focus(Focus::Active), AppComponent::Logger => self.logger.component_state.set_focus(Focus::Active),
AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active), AppComponent::MenuBar => self.menu_bar.component_state.set_focus(Focus::Active),
} }
self.last_active = focus; self.last_active = focus;
} }
@@ -255,21 +253,21 @@ impl<'a> Component for App<'a> {
} }
// Handle events for all components. // Handle events for all components.
let action = match self.last_active { let action = match self.last_active {
AppEditor => self.editor_tab.handle_event(event.clone())?, AppComponent::Editor => self.editor_tab.handle_event(event.clone())?,
AppExplorer => self.explorer.handle_event(event.clone())?, AppComponent::Explorer => self.explorer.handle_event(event.clone())?,
AppLogger => self.logger.handle_event(event.clone())?, AppComponent::Logger => self.logger.handle_event(event.clone())?,
AppMenuBar => self.menu_bar.handle_event(event.clone())?, AppComponent::MenuBar => self.menu_bar.handle_event(event.clone())?,
}; };
// Components should always handle mouse events for click interaction. // Components should always handle mouse events for click interaction.
if let Some(mouse) = event.as_mouse_event() { if let Some(mouse) = event.as_mouse_event()
if mouse.kind == MouseEventKind::Down(MouseButton::Left) { && mouse.kind == MouseEventKind::Down(MouseButton::Left)
if let Some(editor) = self.editor_tab.current_editor_mut() { {
editor.handle_mouse_events(mouse)?; if let Some(editor) = self.editor_tab.current_editor_mut() {
} editor.handle_mouse_events(mouse)?;
self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?;
} }
self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?;
} }
// Handle actions returned from widgets that may need context on other widgets or app state. // Handle actions returned from widgets that may need context on other widgets or app state.
@@ -349,7 +347,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppExplorer); self.change_focus(AppComponent::Explorer);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -358,7 +356,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppEditor); self.change_focus(AppComponent::Editor);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -367,7 +365,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppLogger); self.change_focus(AppComponent::Logger);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -376,7 +374,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppMenuBar); self.change_focus(AppComponent::MenuBar);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {

View File

@@ -7,6 +7,7 @@
use crate::tui::component::Focus::Inactive; use crate::tui::component::Focus::Inactive;
use Focus::Active; use Focus::Active;
use anyhow::Result; use anyhow::Result;
use libclide::theme::colors::Colors;
use log::trace; use log::trace;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
use ratatui::style::Color; use ratatui::style::Color;
@@ -98,8 +99,8 @@ pub enum Focus {
impl Focus { impl Focus {
pub(crate) fn get_active_color(&self) -> Color { pub(crate) fn get_active_color(&self) -> Color {
match self { match self {
Active => Color::LightYellow, Active => Color::from_u32(Colors::css_to_u32(Colors::ACTIVE)?),
Inactive => Color::White, Inactive => Color::from_u32(Colors::css_to_u32(Colors::INACTIVE)?),
} }
} }
} }

View File

@@ -115,9 +115,8 @@ impl Component for Editor {
fn handle_event(&mut self, event: Event) -> Result<Action> { fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler. // Handle events here that should not be passed on to the vim emulation handler.
match self.handle_key_events(key_event)? { if let Action::Handled = self.handle_key_events(key_event)? {
Action::Handled => return Ok(Action::Handled), return Ok(Action::Handled);
_ => {}
} }
} }
self.event_handler.on_event(event, &mut self.state); self.event_handler.on_event(event, &mut self.state);

View File

@@ -4,7 +4,8 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::{trace}; use libclide::fs::entry_meta::EntryMeta;
use log::trace;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect}; use ratatui::layout::{Alignment, Position, Rect};
@@ -14,7 +15,6 @@ use ratatui::widgets::{Block, Borders, StatefulWidget, Widget};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_tree_widget::{Tree, TreeItem, TreeState}; use tui_tree_widget::{Tree, TreeItem, TreeState};
use libclide::fs::entry_meta::EntryMeta;
#[derive(Debug)] #[derive(Debug)]
pub struct Explorer<'a> { pub struct Explorer<'a> {
@@ -30,8 +30,8 @@ impl<'a> Explorer<'a> {
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer { let explorer = Explorer {
root_path: EntryMeta::new(&path)?, root_path: EntryMeta::new(path)?,
tree_items: Self::build_tree_from_path(path.to_owned())?, tree_items: Self::build_tree_from_path(path)?,
tree_state: TreeState::default(), tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
"(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |", "(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |",
@@ -96,7 +96,7 @@ impl<'a> Explorer<'a> {
impl<'a> Widget for &mut Explorer<'a> { impl<'a> Widget for &mut Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if let Ok(tree) = Tree::new(&self.tree_items.children()) { if let Ok(tree) = Tree::new(self.tree_items.children()) {
StatefulWidget::render( StatefulWidget::render(
tree.block( tree.block(
Block::default() Block::default()
@@ -130,23 +130,21 @@ impl<'a> Component for Explorer<'a> {
_ => {} _ => {}
} }
} }
if let Some(mouse_event) = event.as_mouse_event() { if let Some(mouse_event) = event.as_mouse_event()
match self.handle_mouse_events(mouse_event)? { && let Action::Handled = self.handle_mouse_events(mouse_event)?
Action::Handled => return Ok(Action::Handled), {
_ => {} return Ok(Action::Handled);
}
} }
Ok(Action::Pass) Ok(Action::Pass)
} }
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
if key.code == KeyCode::Enter { if key.code == KeyCode::Enter
if let Ok(selected) = self.selected() { && let Ok(selected) = self.selected()
if Path::new(&selected).is_file() { && Path::new(&selected).is_file()
return Ok(Action::OpenTab); {
} // Open a tab if the selected item is a file.
} return Ok(Action::OpenTab);
// Otherwise fall through and handle Enter in the next match case.
} }
let changed = match key.code { let changed = match key.code {

View File

@@ -131,7 +131,7 @@ impl MenuBar {
opened: MenuBarItem, opened: MenuBarItem,
) { ) {
let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10); let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10);
Clear::default().render(popup_area, buf); Clear.render(popup_area, buf);
let options = opened.options().iter().map(|i| ListItem::new(i.id())); let options = opened.options().iter().map(|i| ListItem::new(i.id()));
StatefulWidget::render( StatefulWidget::render(
List::new(options) List::new(options)
@@ -150,15 +150,14 @@ impl MenuBar {
} }
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
let rect = Rect { Rect {
x: anchor.x, x: anchor.x,
y: anchor.y + anchor.height, y: anchor.y + anchor.height,
width: width.min(area.width), width: width.min(area.width),
height, height,
}; }
// TODO: X offset for item option? It's fine as-is, but it might look nicer. // TODO: X offset for item option? It's fine as-is, but it might look nicer.
// trace!(target:Self::ID, "Building Rect under MenuBar popup {}", rect); // trace!(target:Self::ID, "Building Rect under MenuBar popup {}", rect);
rect
} }
} }