Test message 123!

This commit is contained in:
dualfroz committed 2026-06-14 00:22:54 +02:00
1 parent 4cf726709f
commit d4fdc9890a
52 files changed
+5537 -277

No files matched your search

+17
View File
@@ -24,6 +24,21 @@ pub struct AppConfig {
/// Example: ["clients", "internal", "opensource"]
#[serde(default)]
pub workspace_groups: Vec<String>,
/// Shell command used to copy text to clipboard (e.g. "wl-copy" or "xclip -selection clipboard").
#[serde(default = "default_clipboard_cmd")]
pub clipboard_cmd: String,
/// Count lines-of-code during workspace scan (slow for large repos; off by default).
#[serde(default)]
pub show_locs: bool,
}
fn default_clipboard_cmd() -> String {
for cmd in &["wl-copy", "xclip", "pbcopy"] {
if std::process::Command::new("which").arg(cmd).output().map(|o| o.status.success()).unwrap_or(false) {
return if *cmd == "xclip" { "xclip -selection clipboard".into() } else { cmd.to_string() };
}
}
String::new()
}
impl Default for AppConfig {
@@ -44,6 +59,8 @@ impl Default for AppConfig {
timezone: "Europe/Warsaw".into(),
vsc_data_dir: home.join(".fxg/vsc-data"),
workspace_groups: vec![],
clipboard_cmd: default_clipboard_cmd(),
show_locs: false,
}
}
}
+102 -19
View File
@@ -2,6 +2,7 @@ use crate::config::AppConfig;
use crate::tui;
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEvent};
use inquire::Select;
use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
@@ -10,12 +11,31 @@ use ratatui::{
Frame,
};
// Fields that must be chosen from a fixed list — no free text allowed.
const ENUM_FIELDS: &[(&str, &[&str])] = &[
("default_provider", &["github", "gitlab", "gitea", "forgejo"]),
("default_visibility", &["private", "public"]),
("share_stats", &["false", "true"]),
("gitignore_template", &["node", "rust", "python", "blank"]),
];
fn enum_options(key: &str) -> Option<&'static [&'static str]> {
ENUM_FIELDS.iter().find(|(k, _)| *k == key).map(|(_, opts)| *opts)
}
// ── TUI state ─────────────────────────────────────────────────────────────────
enum Action {
Quit,
OpenPicker { field_idx: usize, options: Vec<&'static str> },
}
struct State {
fields: Vec<(&'static str, String)>,
selected: usize,
editing: bool,
edit_buf: String,
quit: bool,
fields: Vec<(&'static str, String)>,
selected: usize,
editing: bool,
edit_buf: String,
action: Option<Action>,
}
impl State {
@@ -25,7 +45,7 @@ impl State {
selected: 0,
editing: false,
edit_buf: String::new(),
quit: false,
action: None,
}
}
@@ -39,6 +59,8 @@ impl State {
}
}
// ── render ────────────────────────────────────────────────────────────────────
fn render(f: &mut Frame, state: &State) {
let area = f.area();
let outer = Block::default()
@@ -54,17 +76,33 @@ fn render(f: &mut Frame, state: &State) {
let items: Vec<ListItem> = state.fields.iter().enumerate().map(|(i, (key, val))| {
let active = i == state.selected;
let is_enum = enum_options(key).is_some();
let value_str = if active && state.editing {
format!("{}_", state.edit_buf)
} else {
val.clone()
};
let value_style = if is_enum {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::White)
};
let key_label = if is_enum {
format!("{:<22} [pick]", key)
} else {
format!("{:<22} ", key)
};
let line = Line::from(vec![
Span::raw(format!(" [{:2}] ", i + 1)),
Span::styled(format!("{:<22}", key), Style::default().fg(Color::Cyan)),
Span::raw(" "),
Span::raw(value_str),
Span::styled(key_label, Style::default().fg(Color::Cyan)),
Span::raw(" "),
Span::styled(value_str, value_style),
]);
let style = if active {
Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD)
} else {
@@ -78,7 +116,7 @@ fn render(f: &mut Frame, state: &State) {
let hint = if state.editing {
" [Enter] confirm [Esc] cancel [Backspace] delete"
} else {
" [↑/↓] navigate [Enter] edit [r] reset [q] save & quit"
" [↑/↓] navigate [Enter] edit / pick [r] reset defaults [q] save & quit"
};
f.render_widget(
Paragraph::new(hint).style(Style::default().fg(Color::DarkGray)),
@@ -86,6 +124,8 @@ fn render(f: &mut Frame, state: &State) {
);
}
// ── key handling ──────────────────────────────────────────────────────────────
fn handle_key(state: &mut State, key: KeyEvent) {
if state.editing {
match key.code {
@@ -104,8 +144,11 @@ fn handle_key(state: &mut State, key: KeyEvent) {
}
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => state.quit = true,
KeyCode::Char('q') | KeyCode::Esc => {
state.action = Some(Action::Quit);
}
KeyCode::Char('r') => {
let defaults = AppConfig::default().fields();
state.fields = defaults;
@@ -117,21 +160,61 @@ fn handle_key(state: &mut State, key: KeyEvent) {
if state.selected + 1 < state.fields.len() { state.selected += 1; }
}
KeyCode::Enter => {
state.edit_buf = state.fields[state.selected].1.clone();
state.editing = true;
let key_name = state.fields[state.selected].0;
if let Some(opts) = enum_options(key_name) {
// Signal the outer loop to open a picker
state.action = Some(Action::OpenPicker {
field_idx: state.selected,
options: opts.to_vec(),
});
} else {
state.edit_buf = state.fields[state.selected].1.clone();
state.editing = true;
}
}
_ => {}
}
}
// ── ratatui inner loop ────────────────────────────────────────────────────────
fn run_ratatui_loop(state: &mut State) -> Result<Action> {
tui::run_loop(|terminal| {
terminal.draw(|f| render(f, state))?;
if let Some(Event::Key(key)) = tui::next_event(250)? {
handle_key(state, key);
}
Ok(state.action.is_some())
})?;
Ok(state.action.take().unwrap_or(Action::Quit))
}
// ── public entry point ────────────────────────────────────────────────────────
pub fn run_config_tui(cfg: AppConfig) -> Result<AppConfig> {
let mut state = State::new(&cfg);
tui::run_loop(|terminal| {
terminal.draw(|f| render(f, &state))?;
if let Some(Event::Key(key)) = tui::next_event(250)? {
handle_key(&mut state, key);
loop {
match run_ratatui_loop(&mut state)? {
Action::Quit => break,
Action::OpenPicker { field_idx, options } => {
// We are now back in normal terminal mode — inquire can draw.
let current = &state.fields[field_idx].1;
let start = options.iter().position(|o| *o == current.as_str()).unwrap_or(0);
let key_name = state.fields[field_idx].0;
match Select::new(&format!("{}:", key_name), options)
.with_starting_cursor(start)
.with_help_message("Use ↑/↓ to select, Enter to confirm, Esc to cancel")
.prompt_skippable()?
{
Some(chosen) => { state.fields[field_idx].1 = chosen.to_string(); }
None => {} // Esc — keep old value
}
// Loop continues — re-enter ratatui
}
}
Ok(state.quit)
})?;
}
Ok(state.to_config(cfg))
}