Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
570 changes: 239 additions & 331 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ tracing-appender = "0.2"
tracing-subscriber = { version = "0.3.20", features = ["json", "env-filter"] }
dirs = "6.0.0"
toml = "0.8"
dark-light = "1.1.1"
dark-light = "2"
oneshot = { version = "0.1.11", default-features = false, features = ["std"] }
tokio = { version = "1.48", default-features = false }
tokio-util = { version = "0.7.17", features = ["codec"] }
Expand Down
15 changes: 15 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@ use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// User preference for the color theme.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThemePreference {
/// Detect automatically from the system setting and follow changes.
#[default]
Auto,
/// Always use the dark palette.
Dark,
/// Always use the light palette.
Light,
}

/// Top-level application configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub keybindings: keybindings::KeybindingConfig,
#[serde(default)]
pub theme: ThemePreference,
}

/// Load configuration from the user's config directory.
Expand Down
8 changes: 6 additions & 2 deletions crates/gui/examples/scrolling_textarea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,18 @@ fn main() {
Box::new(|cc| {
let style = egui::Style {
visuals: match dark_light::detect() {
dark_light::Mode::Dark | dark_light::Mode::Default => {
Ok(dark_light::Mode::Dark) | Ok(dark_light::Mode::Unspecified) => {
tracing::debug!("choosing dark mode");
Visuals::dark()
}
dark_light::Mode::Light => {
Ok(dark_light::Mode::Light) => {
tracing::debug!("choosing light mode");
Visuals::light()
}
Err(e) => {
tracing::warn!(error = %e, "error detecting current theme, defaulting to dark");
Visuals::dark()
}
},
..Default::default()
};
Expand Down
6 changes: 4 additions & 2 deletions crates/gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,17 @@

// Spawn event forwarding thread for this session
std::thread::spawn(move || {
loop {
match event_rx.recv() {
Ok(event) => {
let mut state = app_state.lock().unwrap();
state.handle_session_event(&event);
drop(state);
egui_ctx.request_repaint();
}
Err(_) => break,
}
}

Check warning on line 110 in crates/gui/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

this loop could be written as a `while let` loop

warning: this loop could be written as a `while let` loop --> crates/gui/src/main.rs:100:13 | 100 | / loop { 101 | | match event_rx.recv() { 102 | | Ok(event) => { 103 | | let mut state = app_state.lock().unwrap(); ... | 110 | | } | |_____________^ help: try: `while let Ok(event) = event_rx.recv() { .. }` | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#while_let_loop = note: `#[warn(clippy::while_let_loop)]` on by default
tracing::debug!("event forwarding thread ended");
});

Expand Down Expand Up @@ -336,8 +336,10 @@
Box::new(move |cc| {
let style = egui::Style {
visuals: match dark_light::detect() {
dark_light::Mode::Dark | dark_light::Mode::Default => Visuals::dark(),
dark_light::Mode::Light => Visuals::light(),
Ok(dark_light::Mode::Dark) | Ok(dark_light::Mode::Unspecified) | Err(_) => {
Visuals::dark()
}
Ok(dark_light::Mode::Light) => Visuals::light(),
},
..Default::default()
};
Expand Down
1 change: 1 addition & 0 deletions crates/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fuzzy = { path = "../fuzzy", version = "0.1.0", package = "dap-gui-fuzzy" }
async-transport = { path = "../async-transport", version = "0.1.0", package = "dap-gui-async-transport" }
ui-core = { path = "../ui-core", version = "0.1.0", package = "dap-gui-ui-core" }
config = { path = "../config", version = "0.1.0", package = "dap-gui-config" }
dark-light.workspace = true
clap.workspace = true
eyre.workspace = true
color-eyre.workspace = true
Expand Down
17 changes: 16 additions & 1 deletion crates/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use crate::event::AppEvent;
use crate::line_editor::{InputHistory, LineEditor};
use crate::session::Session;
use crate::theme::{Theme, ThemeMode};
use crossterm::event::KeyEvent;
use launch_configuration::LaunchConfiguration;
use state::StateManager;
Expand Down Expand Up @@ -207,20 +208,24 @@
pub search_history: InputHistory,
pub breakpoint_history: InputHistory,
pub file_browser_history: InputHistory,

// Theme
pub theme: Theme,
}

impl App {
pub fn new(
configs: Vec<LaunchConfiguration>,
config_names: Vec<String>,
selected_config_index: usize,
config_path: PathBuf,
debug_root_dir: PathBuf,
state_manager: StateManager,
wakeup_tx: crossbeam_channel::Sender<()>,
initial_breakpoints: Vec<debugger::Breakpoint>,
keybindings: config::keybindings::KeybindingConfig,
initial_theme: ThemeMode,
) -> Self {

Check warning on line 228 in crates/tui/src/app.rs

View workflow job for this annotation

GitHub Actions / clippy

this function has too many arguments (10/7)

warning: this function has too many arguments (10/7) --> crates/tui/src/app.rs:217:5 | 217 | / pub fn new( 218 | | configs: Vec<LaunchConfiguration>, 219 | | config_names: Vec<String>, 220 | | selected_config_index: usize, ... | 227 | | initial_theme: ThemeMode, 228 | | ) -> Self { | |_____________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#too_many_arguments = note: `#[warn(clippy::too_many_arguments)]` on by default
let kill_ring = Rc::new(RefCell::new(String::new()));
Self {
mode: AppMode::NoSession,
Expand Down Expand Up @@ -275,6 +280,7 @@
search_history: InputHistory::new(),
breakpoint_history: InputHistory::new(),
file_browser_history: InputHistory::new(),
theme: Theme::for_mode(initial_theme),
}
}

Expand All @@ -286,14 +292,16 @@
AppEvent::Tick => self.drain_debugger_events(),
AppEvent::Mouse(_) => {}
AppEvent::Debugger(_) => {} // events arrive via session channel, drained on tick
AppEvent::ThemeChanged(mode) => {
self.theme = Theme::for_mode(mode);
}
}
}

fn handle_key(&mut self, key: KeyEvent) {
crate::input::handle_key(self, key);
}

/// Drain all pending debugger events from the session's channel.
pub fn drain_debugger_events(&mut self) {
// Collect events and errors while briefly borrowing the session
let (events, errors) = {
Expand Down Expand Up @@ -373,14 +381,14 @@
/// Jump code view to the current execution line.
fn jump_to_execution_line(&mut self, state: &debugger::ProgramState) {
let frame = &state.paused_frame.frame;
if let Some(source) = &frame.source {
if let Some(path) = &source.path {
// Open the file if it's different from the current one
if self.code_view.file_path.as_ref() != Some(path) {
self.open_file(path.clone());
}
// Jump cursor to execution line (DAP lines are 1-indexed)
let line = (frame.line as usize).saturating_sub(1);

Check warning on line 391 in crates/tui/src/app.rs

View workflow job for this annotation

GitHub Actions / clippy

casting to the same type is unnecessary (`usize` -> `usize`)

warning: casting to the same type is unnecessary (`usize` -> `usize`) --> crates/tui/src/app.rs:391:28 | 391 | let line = (frame.line as usize).saturating_sub(1); | ^^^^^^^^^^^^^^^^^^^^^ help: try: `frame.line` | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#unnecessary_cast = note: `#[warn(clippy::unnecessary_cast)]` on by default
self.code_view.cursor_line = line;
}
}

Check warning on line 394 in crates/tui/src/app.rs

View workflow job for this annotation

GitHub Actions / clippy

this `if` statement can be collapsed

warning: this `if` statement can be collapsed --> crates/tui/src/app.rs:384:9 | 384 | / if let Some(source) = &frame.source { 385 | | if let Some(path) = &source.path { 386 | | // Open the file if it's different from the current one 387 | | if self.code_view.file_path.as_ref() != Some(path) { ... | 394 | | } | |_________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#collapsible_if = note: `#[warn(clippy::collapsible_if)]` on by default help: collapse nested if block | 384 ~ if let Some(source) = &frame.source 385 ~ && let Some(path) = &source.path { 386 | // Open the file if it's different from the current one ... 392 | self.code_view.cursor_line = line; 393 ~ } |
Expand Down Expand Up @@ -549,17 +557,17 @@
pub fn remove_breakpoint(&mut self, bp: &debugger::Breakpoint) {
self.ui_breakpoints.remove(bp);

if let Some(id) = self.breakpoint_ids.remove(bp) {
if let Some(session) = &self.session {
let result = session
.bridge
.send_sync(|reply| UiCommand::RemoveBreakpoint { id, reply });
if let Err(e) = result {
tracing::warn!(error = %e, "failed to remove breakpoint from debugger");
self.status_error = Some(format!("Remove breakpoint: {e}"));
}
}
}

Check warning on line 570 in crates/tui/src/app.rs

View workflow job for this annotation

GitHub Actions / clippy

this `if` statement can be collapsed

warning: this `if` statement can be collapsed --> crates/tui/src/app.rs:560:9 | 560 | / if let Some(id) = self.breakpoint_ids.remove(bp) { 561 | | if let Some(session) = &self.session { 562 | | let result = session 563 | | .bridge ... | 570 | | } | |_________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#collapsible_if help: collapse nested if block | 560 ~ if let Some(id) = self.breakpoint_ids.remove(bp) 561 ~ && let Some(session) = &self.session { 562 | let result = session ... 568 | } 569 ~ } |

self.persist_breakpoints();
}
Expand Down Expand Up @@ -647,19 +655,19 @@

// Pre-fill with visual selection if active
if self.focus == Focus::CodeView {
if let Some((start, end)) = self.code_view.selection_range() {
if let Some(content) = self.current_file_content() {
let text: String = content
.lines()
.skip(start)
.take(end - start + 1)
.collect::<Vec<_>>()
.join("\n");
self.evaluate_editor.set_text(&text);
self.code_view.selection_anchor = None;
return;
}
}

Check warning on line 670 in crates/tui/src/app.rs

View workflow job for this annotation

GitHub Actions / clippy

this `if` statement can be collapsed

warning: this `if` statement can be collapsed --> crates/tui/src/app.rs:658:13 | 658 | / if let Some((start, end)) = self.code_view.selection_range() { 659 | | if let Some(content) = self.current_file_content() { 660 | | let text: String = content 661 | | .lines() ... | 670 | | } | |_____________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#collapsible_if help: collapse nested if block | 658 ~ if let Some((start, end)) = self.code_view.selection_range() 659 ~ && let Some(content) = self.current_file_content() { 660 | let text: String = content ... 668 | return; 669 ~ } |
// Fall back to trimmed cursor line
if let Some(word) = self.word_under_cursor() {
self.evaluate_editor.set_text(&word);
Expand Down Expand Up @@ -928,6 +936,7 @@
wakeup_tx,
vec![], // no initial breakpoints
Default::default(),
Default::default(),
);

f(&mut app);
Expand Down Expand Up @@ -964,6 +973,7 @@
wakeup_tx,
vec![],
Default::default(),
Default::default(),
);

f(&mut app);
Expand Down Expand Up @@ -1029,6 +1039,7 @@
wakeup_tx,
vec![],
Default::default(),
Default::default(),
);

f(&mut app);
Expand Down Expand Up @@ -1589,6 +1600,7 @@
wakeup_tx,
vec![],
Default::default(),
Default::default(),
);

// Add a breakpoint and persist
Expand All @@ -1612,6 +1624,7 @@
wakeup_tx2,
vec![],
Default::default(),
Default::default(),
);

let restored = ui_core::breakpoints::collect_all_breakpoints(
Expand Down Expand Up @@ -1645,6 +1658,7 @@
wakeup_tx,
vec![],
Default::default(),
Default::default(),
);
app1.ui_breakpoints.insert(debugger::Breakpoint {
name: None,
Expand All @@ -1666,6 +1680,7 @@
wakeup_tx2,
vec![],
Default::default(),
Default::default(),
);
app2.ui_breakpoints.insert(debugger::Breakpoint {
name: None,
Expand Down
49 changes: 47 additions & 2 deletions crates/tui/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@
use crossbeam_channel::{Receiver, Sender};
use crossterm::event::{self, Event, KeyEvent, MouseEvent};

use crate::theme::ThemeMode;

/// All events the application loop can receive.
#[derive(Debug)]
#[allow(dead_code)] // Variants/fields used as phases are implemented
pub enum AppEvent {
/// A key press from the terminal.
Key(KeyEvent),
/// A mouse event from the terminal.
Mouse(MouseEvent),
/// The terminal was resized.
Resize(u16, u16),
/// A debugger event from the async bridge.
Debugger(debugger::Event),
/// Periodic tick for UI refresh (cursor blink, status expiry, etc.).
Tick,
/// The system color scheme changed.
ThemeChanged(ThemeMode),
}

Check warning on line 24 in crates/tui/src/event.rs

View workflow job for this annotation

GitHub Actions / clippy

large size difference between variants

warning: large size difference between variants --> crates/tui/src/event.rs:11:1 | 11 | / pub enum AppEvent { 12 | | /// A key press from the terminal. 13 | | Key(KeyEvent), | | ------------- the second-largest variant contains at least 12 bytes 14 | | /// A mouse event from the terminal. ... | 19 | | Debugger(debugger::Event), | | ------------------------- the largest variant contains at least 440 bytes ... | 23 | | ThemeChanged(ThemeMode), 24 | | } | |_^ the entire enum is at least 440 bytes | = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#large_enum_variant = note: `#[warn(clippy::large_enum_variant)]` on by default help: consider boxing the large fields or introducing indirection in some other way to reduce the total size of the enum | 19 - Debugger(debugger::Event), 19 + Debugger(Box<debugger::Event>), |

/// Background event handler that multiplexes terminal input, debugger events,
/// and periodic ticks into a single channel.
pub struct EventHandler {
rx: Receiver<AppEvent>,
// Keep the handle so the thread is joined on drop.
// Keep handles so threads are joined on drop.
_thread: std::thread::JoinHandle<()>,
_theme_thread: Option<std::thread::JoinHandle<()>>,
}

impl EventHandler {
Expand All @@ -33,7 +38,16 @@
/// `wakeup_rx` receives notifications from the async bridge when debugger
/// events are available. This unblocks the poll wait so the TUI redraws
/// promptly.
pub fn new(tick_rate: Duration, wakeup_rx: Receiver<()>) -> (Self, Sender<AppEvent>) {
///
/// When `theme_preference` is `Auto`, a background thread periodically
/// polls `dark_light::detect()` and sends `ThemeChanged` events when the
/// system color scheme changes.
pub fn new(
tick_rate: Duration,
wakeup_rx: Receiver<()>,
theme_preference: config::ThemePreference,
initial_mode: ThemeMode,
) -> (Self, Sender<AppEvent>) {
let (tx, rx) = crossbeam_channel::unbounded();
let event_tx = tx.clone();

Expand Down Expand Up @@ -87,9 +101,40 @@
})
.expect("failed to spawn event handler thread");

// Spawn a separate thread for theme detection so the blocking D-Bus
// call does not interfere with terminal event polling.
tracing::warn!(?theme_preference, "read theme preference");
let theme_thread = if theme_preference == config::ThemePreference::Auto {
let theme_tx = tx.clone();
Some(
std::thread::Builder::new()
.name("tui-theme-watcher".into())
.spawn(move || {
tracing::warn!("spawning theme watcher thread");
let mut current = initial_mode;
loop {
std::thread::sleep(Duration::from_secs(2));
let detected = crate::theme::detect_theme_mode();
tracing::warn!(?detected, "detected current theme");
if detected != current {
tracing::warn!(?current, ?detected, "switching themes");
current = detected;
if theme_tx.send(AppEvent::ThemeChanged(detected)).is_err() {
break;
}
}
}
})
.expect("failed to spawn theme watcher thread"),
)
} else {
None
};

let handler = Self {
rx,
_thread: thread,
_theme_thread: theme_thread,
};
(handler, tx)
}
Expand Down
16 changes: 15 additions & 1 deletion crates/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod input;
mod line_editor;
mod session;
mod syntax;
pub mod theme;
mod ui;

use app::App;
Expand Down Expand Up @@ -52,6 +53,13 @@ fn main() -> eyre::Result<()> {
// the event handler when debugger events arrive.
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();

let initial_theme = match boot.theme {
config::ThemePreference::Auto => theme::detect_theme_mode(),
config::ThemePreference::Dark => theme::ThemeMode::Dark,
config::ThemePreference::Light => theme::ThemeMode::Light,
};

tracing::warn!(?initial_theme, "got initial theme");
let mut app = App::new(
boot.configs,
boot.config_names,
Expand All @@ -62,6 +70,7 @@ fn main() -> eyre::Result<()> {
wakeup_tx,
boot.initial_breakpoints,
boot.keybindings,
initial_theme,
);

// Install a panic hook that restores the terminal before printing.
Expand All @@ -88,7 +97,12 @@ fn main() -> eyre::Result<()> {
let mut terminal = Terminal::new(backend).wrap_err("creating terminal")?;

// Event loop
let (events, _event_tx) = EventHandler::new(Duration::from_millis(250), wakeup_rx);
let (events, _event_tx) = EventHandler::new(
Duration::from_millis(250),
wakeup_rx,
boot.theme,
initial_theme,
);

let result = run_loop(&mut terminal, &mut app, &events);

Expand Down
Loading
Loading