A graphical client for plain-text protocols written in Rust with GTK. It currently supports the Gemini, Gopher and Finger protocols.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

draw.rs 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. use glib::clone;
  2. use gtk::prelude::*;
  3. use gtk::TextBuffer;
  4. use std::str::FromStr;
  5. use std::sync::Arc;
  6. use url::Url;
  7. use crate::absolute_url::AbsoluteUrl;
  8. use crate::gemini::link::Link as GeminiLink;
  9. use crate::gopher::link::Link as GopherLink;
  10. use crate::gui::Gui;
  11. use crate::protocols::{Finger, Gemini, Gopher};
  12. pub fn gemini_content(
  13. gui: &Arc<Gui>,
  14. content: Vec<Result<crate::gemini::parser::TextElement, crate::gemini::parser::ParseError>>,
  15. ) -> TextBuffer {
  16. let content_view = gui.content_view();
  17. let buffer = content_view.get_buffer().unwrap();
  18. let mut mono_toggle = false;
  19. let font_family = if crate::settings::gemini_monospace() {
  20. "monospace"
  21. } else {
  22. "sans"
  23. };
  24. for el in content {
  25. match el {
  26. Ok(crate::gemini::parser::TextElement::H1(header)) => {
  27. let mut end_iter = buffer.get_end_iter();
  28. buffer.insert_markup(
  29. &mut end_iter,
  30. &format!(
  31. "<span foreground=\"{}\" size=\"x-large\" font_family=\"{}\">{}{}</span>\n",
  32. crate::settings::h1_color(),
  33. font_family,
  34. crate::settings::h1_character(),
  35. escape_text(&header)
  36. ),
  37. );
  38. }
  39. Ok(crate::gemini::parser::TextElement::H2(header)) => {
  40. let mut end_iter = buffer.get_end_iter();
  41. buffer.insert_markup(
  42. &mut end_iter,
  43. &format!(
  44. "<span foreground=\"{}\" size=\"large\" font_family=\"{}\">{}{}</span>\n",
  45. crate::settings::h2_color(),
  46. font_family,
  47. crate::settings::h2_character(),
  48. escape_text(&header)
  49. ),
  50. );
  51. }
  52. Ok(crate::gemini::parser::TextElement::H3(header)) => {
  53. let mut end_iter = buffer.get_end_iter();
  54. buffer.insert_markup(
  55. &mut end_iter,
  56. &format!(
  57. "<span foreground=\"{}\" size=\"medium\" font_family=\"{}\">{}{}</span>\n",
  58. crate::settings::h3_color(),
  59. font_family,
  60. crate::settings::h3_character(),
  61. escape_text(&header)
  62. ),
  63. );
  64. }
  65. Ok(crate::gemini::parser::TextElement::ListItem(item)) => {
  66. let mut end_iter = buffer.get_end_iter();
  67. buffer.insert_markup(
  68. &mut end_iter,
  69. &format!(
  70. "<span foreground=\"{}\" font_family=\"{}\">{} {}</span>\n",
  71. crate::settings::list_color(),
  72. font_family,
  73. crate::settings::list_character(),
  74. escape_text(&item)
  75. ),
  76. );
  77. }
  78. Ok(crate::gemini::parser::TextElement::MonoText(_text)) => {
  79. mono_toggle = !mono_toggle;
  80. }
  81. Ok(crate::gemini::parser::TextElement::Text(text)) => {
  82. let mut end_iter = buffer.get_end_iter();
  83. let text = if text.contains("<span") {
  84. text
  85. } else {
  86. escape_text(&text)
  87. };
  88. if mono_toggle {
  89. buffer.insert_markup(
  90. &mut end_iter,
  91. &format!(
  92. "<span foreground=\"{}\" font_family=\"monospace\">{}</span>\n",
  93. crate::settings::text_color(),
  94. text
  95. ),
  96. );
  97. } else {
  98. buffer.insert_markup(
  99. &mut end_iter,
  100. &format!(
  101. "<span foreground=\"{}\" font_family=\"{}\">{}</span>\n",
  102. crate::settings::text_color(),
  103. font_family,
  104. text
  105. ),
  106. );
  107. }
  108. }
  109. Ok(crate::gemini::parser::TextElement::LinkItem(link_item)) => {
  110. gemini_link(&gui, link_item);
  111. }
  112. Err(_) => println!("Something failed."),
  113. }
  114. }
  115. buffer
  116. }
  117. pub fn gemini_text_content(gui: &Arc<Gui>, content: std::str::Lines) -> TextBuffer {
  118. let content_view = gui.content_view();
  119. let buffer = content_view.get_buffer().unwrap();
  120. for line in content {
  121. let mut end_iter = buffer.get_end_iter();
  122. buffer.insert_markup(
  123. &mut end_iter,
  124. &format!(
  125. "<span foreground=\"{}\" font_family=\"monospace\">{}</span>\n",
  126. crate::settings::text_color(),
  127. escape_text(&line)
  128. ),
  129. );
  130. }
  131. buffer
  132. }
  133. pub fn gopher_content(
  134. gui: &Arc<Gui>,
  135. content: Vec<Result<crate::gopher::parser::TextElement, crate::gopher::parser::ParseError>>,
  136. ) -> TextBuffer {
  137. let content_view = gui.content_view();
  138. let buffer = content_view.get_buffer().unwrap();
  139. for el in content {
  140. match el {
  141. Ok(crate::gopher::parser::TextElement::Text(text)) => {
  142. let mut end_iter = buffer.get_end_iter();
  143. let font_family = if crate::settings::gopher_monospace() {
  144. "font_family=\"monospace\""
  145. } else {
  146. "font_family=\"serif\""
  147. };
  148. let text = if text.contains("<span") {
  149. text
  150. } else {
  151. escape_text(&text)
  152. };
  153. buffer.insert_markup(
  154. &mut end_iter,
  155. &format!(
  156. "<span foreground=\"{}\" {}>{}</span>\n",
  157. crate::settings::text_color(),
  158. font_family,
  159. text
  160. ),
  161. );
  162. }
  163. Ok(crate::gopher::parser::TextElement::LinkItem(link_item)) => {
  164. gopher_link(&gui, link_item);
  165. }
  166. Ok(crate::gopher::parser::TextElement::ExternalLinkItem(link_item)) => {
  167. gopher_link(&gui, link_item);
  168. }
  169. Ok(crate::gopher::parser::TextElement::Image(link_item)) => {
  170. gopher_link(&gui, link_item);
  171. }
  172. Ok(crate::gopher::parser::TextElement::Binary(link_item)) => {
  173. gopher_link(&gui, link_item);
  174. }
  175. Err(_) => println!("Something failed."),
  176. }
  177. }
  178. buffer
  179. }
  180. pub fn finger_content(
  181. gui: &Arc<Gui>,
  182. content: Vec<Result<crate::finger::parser::TextElement, crate::finger::parser::ParseError>>,
  183. ) -> TextBuffer {
  184. let content_view = gui.content_view();
  185. let buffer = content_view.get_buffer().unwrap();
  186. for el in content {
  187. match el {
  188. Ok(crate::finger::parser::TextElement::Text(text)) => {
  189. let mut end_iter = buffer.get_end_iter();
  190. let font_family = if crate::settings::finger_monospace() {
  191. "font_family=\"monospace\""
  192. } else {
  193. "font_family=\"serif\""
  194. };
  195. buffer.insert_markup(
  196. &mut end_iter,
  197. &format!(
  198. "<span foreground=\"{}\" {}>{}</span>\n",
  199. crate::settings::text_color(),
  200. font_family,
  201. escape_text(&text)
  202. ),
  203. );
  204. }
  205. Err(_) => println!("Something failed."),
  206. }
  207. }
  208. buffer
  209. }
  210. pub fn gemini_link(gui: &Arc<Gui>, link_item: String) {
  211. match GeminiLink::from_str(&link_item) {
  212. Ok(GeminiLink::Finger(url, label)) => {
  213. let button_label = if label.is_empty() {
  214. url.clone().to_string()
  215. } else {
  216. label
  217. };
  218. let finger_label = format!("{} [Finger]", button_label);
  219. insert_button(&gui, url, finger_label);
  220. }
  221. Ok(GeminiLink::Gemini(url, label)) => {
  222. insert_button(&gui, url, label);
  223. }
  224. Ok(GeminiLink::Gopher(url, label)) => {
  225. let button_label = if label.is_empty() {
  226. url.clone().to_string()
  227. } else {
  228. label
  229. };
  230. let gopher_label = format!("{} [Gopher]", button_label);
  231. insert_button(&gui, url, gopher_label);
  232. }
  233. Ok(GeminiLink::Http(url, label)) => {
  234. let button_label = if label.is_empty() {
  235. url.clone().to_string()
  236. } else {
  237. label
  238. };
  239. let www_label = format!("{} [WWW]", button_label);
  240. insert_external_button(&gui, url, &www_label);
  241. }
  242. Ok(GeminiLink::Relative(url, label)) => {
  243. let new_url = Gemini { source: url }.to_absolute_url().unwrap();
  244. insert_button(&gui, new_url, label);
  245. }
  246. Ok(GeminiLink::Unknown(_, _)) => (),
  247. Err(_) => (),
  248. }
  249. }
  250. pub fn gopher_link(gui: &Arc<Gui>, link_item: String) {
  251. match GopherLink::from_str(&link_item) {
  252. Ok(GopherLink::Http(url, label)) => {
  253. let button_label = if label.is_empty() {
  254. url.clone().to_string()
  255. } else {
  256. label
  257. };
  258. let www_label = format!("{} [WWW]", button_label);
  259. insert_external_button(&gui, url, &www_label);
  260. }
  261. Ok(GopherLink::Gopher(url, label)) => {
  262. let button_label = if label.is_empty() {
  263. url.clone().to_string()
  264. } else {
  265. label
  266. };
  267. let gopher_label = format!("{} [Gopher]", button_label);
  268. insert_button(&gui, url, gopher_label);
  269. }
  270. Ok(GopherLink::Image(url, label)) => {
  271. let button_label = if label.is_empty() {
  272. url.clone().to_string()
  273. } else {
  274. label
  275. };
  276. let image_label = format!("{} [Image]", button_label);
  277. insert_gopher_file_button(&gui, url, image_label);
  278. }
  279. Ok(GopherLink::File(url, label)) => {
  280. let button_label = if label.is_empty() {
  281. url.clone().to_string()
  282. } else {
  283. label
  284. };
  285. let file_label = format!("{} [File]", button_label);
  286. insert_gopher_file_button(&gui, url, file_label);
  287. }
  288. Ok(GopherLink::Gemini(url, label)) => {
  289. insert_button(&gui, url, label);
  290. }
  291. Ok(GopherLink::Relative(url, label)) => {
  292. let new_url = Gopher { source: url }.to_absolute_url().unwrap();
  293. insert_button(&gui, new_url, label);
  294. }
  295. Ok(GopherLink::Ftp(url, label)) => {
  296. let button_label = if label.is_empty() {
  297. url.clone().to_string()
  298. } else {
  299. label
  300. };
  301. let ftp_label = format!("{} [FTP]", button_label);
  302. insert_external_button(&gui, url, &ftp_label);
  303. }
  304. Ok(GopherLink::Finger(url, label)) => {
  305. let button_label = if label.is_empty() {
  306. url.clone().to_string()
  307. } else {
  308. label
  309. };
  310. let finger_label = format!("{} [Finger]", button_label);
  311. insert_external_button(&gui, url, &finger_label);
  312. }
  313. Ok(GopherLink::Unknown(_, _)) => (),
  314. Err(_) => (),
  315. }
  316. }
  317. pub fn insert_button(gui: &Arc<Gui>, url: Url, label: String) {
  318. let content_view = gui.content_view();
  319. let buffer = content_view.get_buffer().unwrap();
  320. let button_label = if label.is_empty() {
  321. url.clone().to_string()
  322. } else {
  323. label
  324. };
  325. let button = gtk::Button::new_with_label(&button_label);
  326. button.set_tooltip_text(Some(&url.to_string()));
  327. button.connect_clicked(clone!(@weak gui => move |_| {
  328. match url.scheme() {
  329. "finger" => crate::visit_url(&gui, Finger { source: url.to_string() }),
  330. "gemini" => crate::visit_url(&gui, Gemini { source: url.to_string() }),
  331. "gopher" => crate::visit_url(&gui, Gopher { source: url.to_string() }),
  332. _ => ()
  333. }
  334. }));
  335. let mut start_iter = buffer.get_end_iter();
  336. let anchor = buffer.create_child_anchor(&mut start_iter).unwrap();
  337. content_view.add_child_at_anchor(&button, &anchor);
  338. let mut end_iter = buffer.get_end_iter();
  339. buffer.insert(&mut end_iter, "\n");
  340. }
  341. pub fn insert_gopher_file_button(gui: &Arc<Gui>, url: Url, label: String) {
  342. let content_view = gui.content_view();
  343. let buffer = content_view.get_buffer().unwrap();
  344. let button_label = if label.is_empty() {
  345. url.clone().to_string()
  346. } else {
  347. label
  348. };
  349. let button = gtk::Button::new_with_label(&button_label);
  350. button.set_tooltip_text(Some(&url.to_string()));
  351. button.connect_clicked(move |_| {
  352. let (_meta, content) = crate::gopher::client::get_data(Gopher {
  353. source: url.to_string(),
  354. })
  355. .unwrap();
  356. crate::client::download(content);
  357. });
  358. let mut start_iter = buffer.get_end_iter();
  359. let anchor = buffer.create_child_anchor(&mut start_iter).unwrap();
  360. content_view.add_child_at_anchor(&button, &anchor);
  361. let mut end_iter = buffer.get_end_iter();
  362. buffer.insert(&mut end_iter, "\n");
  363. }
  364. pub fn insert_external_button(gui: &Arc<Gui>, url: Url, label: &str) {
  365. let content_view = gui.content_view();
  366. let buffer = content_view.get_buffer().unwrap();
  367. let button = gtk::Button::new_with_label(&label);
  368. button.set_tooltip_text(Some(&url.to_string()));
  369. button.connect_clicked(move |_| {
  370. open::that(url.to_string()).unwrap();
  371. });
  372. let mut start_iter = buffer.get_end_iter();
  373. let anchor = buffer.create_child_anchor(&mut start_iter).unwrap();
  374. content_view.add_child_at_anchor(&button, &anchor);
  375. let mut end_iter = buffer.get_end_iter();
  376. buffer.insert(&mut end_iter, "\n");
  377. }
  378. fn escape_text(str: &str) -> String {
  379. String::from(glib::markup_escape_text(&str).as_str())
  380. }