open_file: file associations, default editor and 'Open with' dialog
- New open_file IPC message: resolves [open_with] extension mapping,
falls back to default_editor for known text formats, otherwise opens
an 'Öppna med' dialog where the user types a command (live preview
of the full command line, path tail-truncated so the extension stays
visible) and chooses open-once or save-as-default for the extension
(persisted to [open_with] in config.toml)
- default_editor configurable in the settings dialog (new Editor row
with inline text editing) and in config.toml; '{}' in commands is
replaced with the shell-quoted path
- Documented in SOCKET_API.md; integration suite covers dialog →
save-association → direct open (38/38)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
202
src/app.rs
202
src/app.rs
@@ -308,10 +308,22 @@ pub enum WindowContent {
|
||||
input: String,
|
||||
cursor_pos: usize,
|
||||
},
|
||||
/// Inbyggd inställningsdialog (tema, panel, skugga, scrollbar)
|
||||
/// Inbyggd inställningsdialog (tema, panel, editor, skugga, scrollbar)
|
||||
Settings {
|
||||
/// Markerad rad (0=tema, 1=panel, 2=skugga, 3=scrollbar)
|
||||
/// Markerad rad (0=tema, 1=panel, 2=editor, 3=skugga, 4=scrollbar)
|
||||
selected: usize,
|
||||
/// Textredigering pågår för editor-raden (buffern som skrivs i)
|
||||
editing: Option<String>,
|
||||
},
|
||||
/// "Öppna med"-dialog: fråga vilken app som ska öppna en fil vars
|
||||
/// format saknar association.
|
||||
OpenWith {
|
||||
/// Filen som ska öppnas
|
||||
path: String,
|
||||
/// Kommandot som skrivs in
|
||||
input: String,
|
||||
/// Spara som standard för filformatet (Tab växlar)
|
||||
save_default: bool,
|
||||
},
|
||||
PopupDialog {
|
||||
message: String,
|
||||
@@ -731,6 +743,7 @@ impl App {
|
||||
WindowContent::RunDialog { .. } => true,
|
||||
WindowContent::PopupDialog { .. } => true,
|
||||
WindowContent::Settings { .. } => true,
|
||||
WindowContent::OpenWith { .. } => true,
|
||||
});
|
||||
let alive_ids: Vec<usize> = self.windows.iter().map(|w| w.id).collect();
|
||||
self.focus_history.retain(|h| alive_ids.contains(h));
|
||||
@@ -854,6 +867,16 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
// 2a1b. "Öppna med"-dialogen hanterar sina egna tangenter
|
||||
if let Some(id) = self.focused_id {
|
||||
let is_ow = self.windows.iter()
|
||||
.any(|w| w.id == id && matches!(w.content, WindowContent::OpenWith { .. }));
|
||||
if is_ow {
|
||||
self.handle_open_with_key(id, key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2a2. Inställningsdialogen hanterar sina egna tangenter
|
||||
if let Some(id) = self.focused_id {
|
||||
let is_settings = self.windows.iter()
|
||||
@@ -1318,10 +1341,10 @@ impl App {
|
||||
self.focus_window(id);
|
||||
// rad 1..=4 i innehållet motsvarar inställningsrad 0..=3
|
||||
let rel = row.saturating_sub(cr.y);
|
||||
if (1..=4).contains(&rel) {
|
||||
if (1..=5).contains(&rel) {
|
||||
let target = (rel - 1) as usize;
|
||||
let mut activate = false;
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) {
|
||||
if *selected == target {
|
||||
activate = true;
|
||||
} else {
|
||||
@@ -1721,6 +1744,108 @@ impl App {
|
||||
id
|
||||
}
|
||||
|
||||
/// Kända text-ändelser som öppnas med default_editor.
|
||||
const TEXT_EXTENSIONS: &'static [&'static str] = &[
|
||||
"txt", "md", "rs", "py", "js", "ts", "jsx", "tsx", "c", "cpp", "h",
|
||||
"hpp", "toml", "json", "yaml", "yml", "xml", "html", "css", "sh",
|
||||
"bash", "zsh", "fish", "log", "conf", "cfg", "ini", "csv", "sql",
|
||||
"go", "java", "kt", "lua", "rb", "php", "vim", "tex", "gitignore",
|
||||
"lock", "service", "desktop",
|
||||
];
|
||||
|
||||
/// Öppna en fil: filassociation ([open_with]) vinner, textfiler går
|
||||
/// till default_editor, annars visas "Öppna med"-dialogen.
|
||||
/// Returnerar id för fönstret/dialogen som skapades.
|
||||
pub fn open_file(&mut self, path: &str) -> usize {
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
if let Some(cmd) = self.config.open_with.get(&ext).cloned() {
|
||||
return self.spawn_terminal(&build_open_command(&cmd, path));
|
||||
}
|
||||
if Self::TEXT_EXTENSIONS.contains(&ext.as_str()) {
|
||||
if let Some(ed) = self.config.default_editor.clone() {
|
||||
return self.spawn_terminal(&build_open_command(&ed, path));
|
||||
}
|
||||
}
|
||||
self.spawn_open_with_dialog(path.to_string())
|
||||
}
|
||||
|
||||
/// Öppna "Öppna med"-dialogen för en fil utan association.
|
||||
fn spawn_open_with_dialog(&mut self, path: String) -> usize {
|
||||
let ca = self.content_area;
|
||||
let w = 64u16.min(ca.width.saturating_sub(2));
|
||||
let h = 10u16.min(ca.height.saturating_sub(2));
|
||||
let x = ca.x as i32 + ((ca.width.saturating_sub(w)) / 2) as i32;
|
||||
let y = ca.y as i32 + ((ca.height.saturating_sub(h)) / 2) as i32;
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.windows.push(FloatingWindow {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
content: WindowContent::OpenWith { path, input: String::new(), save_default: false },
|
||||
dragging: None,
|
||||
resizing: None,
|
||||
resizable: false,
|
||||
mouse_mode: vt100::MouseProtocolMode::None,
|
||||
mouse_encoding: vt100::MouseProtocolEncoding::Default,
|
||||
mouse_seq_carry: Vec::new(),
|
||||
selection: None,
|
||||
kitty_gfx_carry: Vec::new(),
|
||||
pending_graphics: Vec::new(),
|
||||
scrollback_total: 0,
|
||||
prev_geom: None,
|
||||
pty_log: Vec::new(),
|
||||
});
|
||||
self.focus_window(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn handle_open_with_key(&mut self, id: usize, key: KeyEvent) {
|
||||
// Plocka ut fälten utan långlivat lån
|
||||
let Some(w) = self.windows.iter_mut().find(|w| w.id == id) else { return };
|
||||
let WindowContent::OpenWith { path, input, save_default } = &mut w.content else { return };
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.close_window(id);
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
*save_default = !*save_default;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let cmd = input.trim().to_string();
|
||||
if cmd.is_empty() {
|
||||
return;
|
||||
}
|
||||
let path = path.clone();
|
||||
let save = *save_default;
|
||||
self.close_window(id);
|
||||
if save {
|
||||
let ext = std::path::Path::new(&path)
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
if !ext.is_empty() {
|
||||
self.config.open_with.insert(ext.clone(), cmd.clone());
|
||||
let _ = Config::save_open_with("config.toml", &ext, &cmd);
|
||||
}
|
||||
}
|
||||
self.spawn_terminal(&build_open_command(&cmd, &path));
|
||||
}
|
||||
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
input.push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Öppna inställningsdialogen (eller fokusera en redan öppen).
|
||||
fn spawn_settings(&mut self) {
|
||||
if let Some(w) = self.windows.iter().find(|w| matches!(w.content, WindowContent::Settings { .. })) {
|
||||
@@ -1741,7 +1866,7 @@ impl App {
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
content: WindowContent::Settings { selected: 0 },
|
||||
content: WindowContent::Settings { selected: 0, editing: None },
|
||||
dragging: None,
|
||||
resizing: None,
|
||||
resizable: false,
|
||||
@@ -1759,16 +1884,49 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_settings_key(&mut self, id: usize, key: KeyEvent) {
|
||||
const ROWS: usize = 4;
|
||||
const ROWS: usize = 5;
|
||||
// Textredigering av editor-raden pågår?
|
||||
if let Some(WindowContent::Settings { editing, .. }) = self.settings_content(id) {
|
||||
if editing.is_some() {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
let buf = editing.take().unwrap_or_default();
|
||||
let val = buf.trim().to_string();
|
||||
self.config.default_editor =
|
||||
if val.is_empty() { None } else { Some(val) };
|
||||
let _ = Config::save_top_level_str(
|
||||
"config.toml",
|
||||
"default_editor",
|
||||
self.config.default_editor.as_deref(),
|
||||
);
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
*editing = None;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
if let Some(b) = editing.as_mut() {
|
||||
b.pop();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
if let Some(b) = editing.as_mut() {
|
||||
b.push(c);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => self.close_window(id),
|
||||
KeyCode::Up => {
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) {
|
||||
*selected = (*selected + ROWS - 1) % ROWS;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => {
|
||||
if let Some(WindowContent::Settings { selected }) = self.settings_content(id) {
|
||||
if let Some(WindowContent::Settings { selected, .. }) = self.settings_content(id) {
|
||||
*selected = (*selected + 1) % ROWS;
|
||||
}
|
||||
}
|
||||
@@ -1800,8 +1958,14 @@ impl App {
|
||||
/// Ändringar skrivs till config.toml — hot-reload plockar upp dem
|
||||
/// direkt, men tema/flaggor appliceras även omedelbart i minnet.
|
||||
fn settings_activate(&mut self, id: usize, forward: bool) {
|
||||
let Some(WindowContent::Settings { selected }) = self.settings_content(id) else { return };
|
||||
let current_editor = self.config.default_editor.clone().unwrap_or_default();
|
||||
let Some(WindowContent::Settings { selected, editing }) = self.settings_content(id) else { return };
|
||||
let row = *selected;
|
||||
if row == 2 {
|
||||
// Editor: börja textredigera värdet
|
||||
*editing = Some(current_editor);
|
||||
return;
|
||||
}
|
||||
match row {
|
||||
0 => {
|
||||
// Tema: cykla inbyggt + themes/*.toml
|
||||
@@ -1836,11 +2000,11 @@ impl App {
|
||||
let _ = Config::save_panel_file("config.toml", 0, &options[next]);
|
||||
// panel-innehållet läses om av hot-reloaden
|
||||
}
|
||||
2 => {
|
||||
3 => {
|
||||
self.config.shadow = !self.config.shadow;
|
||||
let _ = Config::save_top_level_bool("config.toml", "shadow", self.config.shadow);
|
||||
}
|
||||
3 => {
|
||||
4 => {
|
||||
self.config.show_scrollbar = !self.config.show_scrollbar;
|
||||
let _ = Config::save_top_level_bool("config.toml", "show_scrollbar", self.config.show_scrollbar);
|
||||
}
|
||||
@@ -2383,6 +2547,22 @@ fn in_rect(col: u16, row: u16, rect: Rect) -> bool {
|
||||
&& row < rect.y + rect.height
|
||||
}
|
||||
|
||||
/// Shell-citera en sökväg för sh -c.
|
||||
fn shell_quote(p: &str) -> String {
|
||||
format!("'{}'", p.replace('\'', r"'\''"))
|
||||
}
|
||||
|
||||
/// Bygg kommandot som öppnar `path` med `cmd`: "{}" ersätts med citerad
|
||||
/// sökväg, annars läggs sökvägen sist.
|
||||
pub fn build_open_command(cmd: &str, path: &str) -> String {
|
||||
let quoted = shell_quote(path);
|
||||
if cmd.contains("{}") {
|
||||
cmd.replace("{}", "ed)
|
||||
} else {
|
||||
format!("{} {}", cmd, quoted)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
// Försök hitta PowerShell i prioritetsordning: pwsh (Core) → powershell (inbyggd)
|
||||
|
||||
@@ -16,6 +16,13 @@ pub struct Config {
|
||||
/// Sökväg till temafil (t.ex. "themes/dark.toml"). Om None → inbyggt tema.
|
||||
#[serde(default)]
|
||||
pub theme: Option<String>,
|
||||
/// Standard-editor för textfiler (open_file via socket/TUI-FM).
|
||||
/// "{}" i kommandot ersätts med filens sökväg, annars läggs den sist.
|
||||
#[serde(default)]
|
||||
pub default_editor: Option<String>,
|
||||
/// Filassociationer: ändelse (gemener) → kommando för open_file.
|
||||
#[serde(default)]
|
||||
pub open_with: std::collections::HashMap<String, String>,
|
||||
/// Rita skugga under/till höger om fönster.
|
||||
#[serde(default = "default_true")]
|
||||
pub shadow: bool,
|
||||
@@ -280,6 +287,8 @@ impl Config {
|
||||
terminal_bg_color: None,
|
||||
default_shell: None,
|
||||
theme: None,
|
||||
default_editor: None,
|
||||
open_with: std::collections::HashMap::new(),
|
||||
shadow: true,
|
||||
scrollback_lines: default_scrollback_lines(),
|
||||
show_scrollbar: default_true(),
|
||||
@@ -316,6 +325,18 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spara en filassociation ([open_with] ändelse → kommando).
|
||||
pub fn save_open_with(path: &str, ext: &str, cmd: &str) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
let mut doc: toml_edit::DocumentMut = content.parse().unwrap_or_default();
|
||||
if doc.get("open_with").is_none() {
|
||||
doc["open_with"] = toml_edit::Item::Table(toml_edit::Table::new());
|
||||
}
|
||||
doc["open_with"][ext] = toml_edit::value(cmd);
|
||||
fs::write(path, doc.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sätt fil-referensen för panel nummer `index` i config-filen.
|
||||
pub fn save_panel_file(path: &str, index: usize, file: &str) -> anyhow::Result<()> {
|
||||
let content = fs::read_to_string(path).unwrap_or_default();
|
||||
|
||||
@@ -84,6 +84,13 @@ pub enum ClientMessage {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
request_id: Option<String>,
|
||||
},
|
||||
/// Be TUI-WM öppna en fil: filassociation eller standard-editor
|
||||
/// används om möjligt, annars visas "Öppna med"-dialogen.
|
||||
OpenFile {
|
||||
path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
request_id: Option<String>,
|
||||
},
|
||||
SetBackground {
|
||||
/// Sökväg till bildfil, eller tom/null för att ta bort bakgrund
|
||||
#[serde(default)]
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -597,6 +597,9 @@ fn handle_ipc_event(
|
||||
format!("[Popup: {}]", &message[..message.len().min(20)])
|
||||
}
|
||||
WindowContent::Settings { .. } => "[Inställningar]".to_string(),
|
||||
WindowContent::OpenWith { path, .. } => {
|
||||
format!("[Öppna: {}]", path)
|
||||
}
|
||||
};
|
||||
Some(WindowInfo {
|
||||
id: w.id,
|
||||
@@ -615,6 +618,14 @@ fn handle_ipc_event(
|
||||
ClientMessage::CloseWindow { window_id, .. } => {
|
||||
app.close_window_pub(window_id);
|
||||
}
|
||||
ClientMessage::OpenFile { path, request_id } => {
|
||||
let window_id = app.open_file(&path);
|
||||
app.ipc_out.push(AppIpcOut::WindowOpened {
|
||||
client_id,
|
||||
request_id,
|
||||
window_id,
|
||||
});
|
||||
}
|
||||
ClientMessage::SetBackground { path, save, request_id } => {
|
||||
app.config.background_image = path.clone();
|
||||
if save {
|
||||
|
||||
@@ -78,6 +78,7 @@ fn render_switcher(frame: &mut Frame, app: &App, sw: &crate::app::SwitcherState)
|
||||
WindowContent::RunDialog { .. } => "Kommandodialog".to_string(),
|
||||
WindowContent::PopupDialog { .. } => "Popup".to_string(),
|
||||
WindowContent::Settings { .. } => "Inställningar".to_string(),
|
||||
WindowContent::OpenWith { .. } => "Öppna med".to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| format!("fönster {}", id))
|
||||
})
|
||||
@@ -300,6 +301,7 @@ fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) {
|
||||
WindowContent::RunDialog { .. } => " Tui-run ".to_string(),
|
||||
WindowContent::PopupDialog { .. } => " TUI-WM ".to_string(),
|
||||
WindowContent::Settings { .. } => " Inställningar ".to_string(),
|
||||
WindowContent::OpenWith { .. } => " Öppna med ".to_string(),
|
||||
};
|
||||
|
||||
// Diskret skugga nedanför/till höger om fönstret (inte när en
|
||||
@@ -388,8 +390,11 @@ fn render_window(frame: &mut Frame, window: &FloatingWindow, app: &App) {
|
||||
WindowContent::PopupDialog { .. } => {
|
||||
render_popup_dialog(frame, window, focused_id, th);
|
||||
}
|
||||
WindowContent::Settings { selected } => {
|
||||
render_settings_content(frame, cr, *selected, app, th);
|
||||
WindowContent::Settings { selected, editing } => {
|
||||
render_settings_content(frame, cr, *selected, editing.as_deref(), app, th);
|
||||
}
|
||||
WindowContent::OpenWith { path, input, save_default } => {
|
||||
render_open_with_content(frame, cr, path, input, *save_default, th);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +521,7 @@ fn render_settings_content(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
selected: usize,
|
||||
editing: Option<&str>,
|
||||
app: &App,
|
||||
th: &crate::theme::Theme,
|
||||
) {
|
||||
@@ -526,10 +532,19 @@ fn render_settings_content(
|
||||
.first()
|
||||
.and_then(|p| p.file.as_deref())
|
||||
.unwrap_or("(inline)");
|
||||
let editor_val = match editing {
|
||||
Some(buf) => format!("{}▏ (Enter=spara, Esc=avbryt)", buf),
|
||||
None => app
|
||||
.config
|
||||
.default_editor
|
||||
.clone()
|
||||
.unwrap_or_else(|| "(ingen — Enter för att ange)".to_string()),
|
||||
};
|
||||
let onoff = |b: bool| if b { "[x] på " } else { "[ ] av " };
|
||||
let rows: Vec<(String, String)> = vec![
|
||||
("Tema".to_string(), format!("‹ {} ›", theme_name)),
|
||||
("Panel".to_string(), format!("‹ {} ›", panel_name)),
|
||||
("Editor".to_string(), editor_val),
|
||||
("Skugga".to_string(), onoff(app.config.shadow).to_string()),
|
||||
("Scrollbar".to_string(), onoff(app.config.show_scrollbar).to_string()),
|
||||
];
|
||||
@@ -576,6 +591,85 @@ fn render_settings_content(
|
||||
}
|
||||
}
|
||||
|
||||
/// Trunkera en sträng från vänster så att slutet (med filändelsen)
|
||||
/// alltid syns: "…/lång/sökväg/fil.txt".
|
||||
fn tail_fit(s: &str, max_cols: usize) -> String {
|
||||
let w = unicode_width::UnicodeWidthStr::width(s);
|
||||
if w <= max_cols {
|
||||
return s.to_string();
|
||||
}
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let mut out = String::new();
|
||||
let mut used = 1; // för "…"
|
||||
for &c in chars.iter().rev() {
|
||||
let cw = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
|
||||
if used + cw > max_cols {
|
||||
break;
|
||||
}
|
||||
used += cw;
|
||||
out.insert(0, c);
|
||||
}
|
||||
format!("…{}", out)
|
||||
}
|
||||
|
||||
/// Innehållet i "Öppna med"-dialogen.
|
||||
fn render_open_with_content(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
path: &str,
|
||||
input: &str,
|
||||
save_default: bool,
|
||||
th: &crate::theme::Theme,
|
||||
) {
|
||||
let w = area.width as usize;
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
|
||||
let mut y = area.y;
|
||||
let mut line = |frame: &mut Frame, y: u16, spans: Vec<Span>| {
|
||||
if y < area.y + area.height {
|
||||
frame.render_widget(Paragraph::new(Line::from(spans)), Rect::new(area.x, y, area.width, 1));
|
||||
}
|
||||
};
|
||||
|
||||
// Fil (svans-trunkerad så ändelsen syns)
|
||||
line(frame, y, vec![
|
||||
Span::styled(" Fil: ", Style::default().fg(th.hint)),
|
||||
Span::styled(tail_fit(path, w.saturating_sub(12)), Style::default().fg(th.text)),
|
||||
]);
|
||||
y += 2;
|
||||
// Kommando-input
|
||||
line(frame, y, vec![
|
||||
Span::styled(" Kommando: ", Style::default().fg(th.hint)),
|
||||
Span::styled(input.to_string(), Style::default().fg(th.text)),
|
||||
Span::styled("▏", Style::default().fg(th.accent)),
|
||||
]);
|
||||
y += 1;
|
||||
// Förhandsgranskning av det fullständiga kommandot
|
||||
let preview = if input.trim().is_empty() {
|
||||
"(skriv ett kommando — \"{}\" ersätts med sökvägen)".to_string()
|
||||
} else {
|
||||
crate::app::build_open_command(input.trim(), path)
|
||||
};
|
||||
line(frame, y, vec![
|
||||
Span::styled(" Kör: ", Style::default().fg(th.hint)),
|
||||
Span::styled(tail_fit(&preview, w.saturating_sub(12)), Style::default().fg(th.ok)),
|
||||
]);
|
||||
y += 2;
|
||||
// Spara som standard-kryssruta
|
||||
let check = if save_default { "[x]" } else { "[ ]" };
|
||||
line(frame, y, vec![
|
||||
Span::styled(format!(" {} Spara som standard för .{}", check, ext),
|
||||
if save_default { Style::default().fg(th.accent) } else { Style::default().fg(th.text) }),
|
||||
]);
|
||||
y += 2;
|
||||
line(frame, y, vec![
|
||||
Span::styled(" Enter=Öppna Tab=växla spara Esc=avbryt", Style::default().fg(th.hint)),
|
||||
]);
|
||||
}
|
||||
|
||||
fn render_popup_dialog(
|
||||
frame: &mut Frame,
|
||||
window: &FloatingWindow,
|
||||
|
||||
Reference in New Issue
Block a user