bouquin 0.2.1__tar.gz → 0.2.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bouquin
3
- Version: 0.2.1
3
+ Version: 0.2.1.2
4
4
  Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
5
5
  Home-page: https://git.mig5.net/mig5/bouquin
6
6
  License: GPL-3.0-or-later
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import csv
4
4
  import html
5
5
  import json
6
- import os
7
6
 
8
7
  from dataclasses import dataclass
9
8
  from pathlib import Path
@@ -427,29 +426,6 @@ class DBManager:
427
426
  cur.execute("SELECT sqlcipher_export('backup')")
428
427
  cur.execute("DETACH DATABASE backup")
429
428
 
430
- def export_by_extension(self, file_path: str) -> None:
431
- """
432
- Fallback catch-all that runs one of the above functions based on
433
- the extension of the file name that was chosen by the user.
434
- """
435
- entries = self.get_all_entries()
436
- ext = os.path.splitext(file_path)[1].lower()
437
-
438
- if ext == ".json":
439
- self.export_json(entries, file_path)
440
- elif ext == ".csv":
441
- self.export_csv(entries, file_path)
442
- elif ext == ".txt":
443
- self.export_txt(entries, file_path)
444
- elif ext in {".html", ".htm"}:
445
- self.export_html(entries, file_path)
446
- elif ext in {".sql", ".sqlite"}:
447
- self.export_sql(file_path)
448
- elif ext == ".md":
449
- self.export_markdown(entries, file_path)
450
- else:
451
- raise ValueError(f"Unsupported extension: {ext}")
452
-
453
429
  def compact(self) -> None:
454
430
  """
455
431
  Runs VACUUM on the db.
@@ -21,9 +21,8 @@ from PySide6.QtWidgets import (
21
21
  class FindBar(QWidget):
22
22
  """Widget for finding text in the Editor"""
23
23
 
24
- closed = (
25
- Signal()
26
- ) # emitted when the bar is hidden (Esc/✕), so caller can refocus editor
24
+ # emitted when the bar is hidden (Esc/✕), so caller can refocus editor
25
+ closed = Signal()
27
26
 
28
27
  def __init__(
29
28
  self,
@@ -45,7 +44,7 @@ class FindBar(QWidget):
45
44
  layout.addWidget(QLabel("Find:"))
46
45
 
47
46
  self.edit = QLineEdit(self)
48
- self.edit.setPlaceholderText("Type to search")
47
+ self.edit.setPlaceholderText("Type to search")
49
48
  layout.addWidget(self.edit)
50
49
 
51
50
  self.case = QCheckBox("Match case", self)
@@ -79,7 +78,7 @@ class FindBar(QWidget):
79
78
 
80
79
  @property
81
80
  def editor(self) -> QTextEdit | None:
82
- """Get the current editor (no side effects)."""
81
+ """Get the current editor"""
83
82
  return self._editor_getter()
84
83
 
85
84
  # ----- Public API -----
@@ -118,9 +118,9 @@ class HistoryDialog(QDialog):
118
118
  return local.strftime("%Y-%m-%d %H:%M:%S %Z")
119
119
 
120
120
  def _load_versions(self):
121
- self._versions = self._db.list_versions(
122
- self._date
123
- ) # [{id,version_no,created_at,note,is_current}]
121
+ # [{id,version_no,created_at,note,is_current}]
122
+ self._versions = self._db.list_versions(self._date)
123
+
124
124
  self._current_id = next(
125
125
  (v["id"] for v in self._versions if v["is_current"]), None
126
126
  )
@@ -152,13 +152,8 @@ class HistoryDialog(QDialog):
152
152
  self.btn_revert.setEnabled(False)
153
153
  return
154
154
  sel_id = item.data(Qt.UserRole)
155
- # Preview selected as plain text (markdown)
156
155
  sel = self._db.get_version(version_id=sel_id)
157
- # Show markdown as plain text with monospace font for better readability
158
- self.preview.setPlainText(sel["content"])
159
- self.preview.setStyleSheet(
160
- "font-family: Consolas, Menlo, Monaco, monospace; font-size: 13px;"
161
- )
156
+ self.preview.setMarkdown(sel["content"])
162
157
  # Diff vs current (textual diff)
163
158
  cur = self._db.get_version(version_id=self._current_id)
164
159
  self.diff.setHtml(_colored_unified_diff_html(cur["content"], sel["content"]))
@@ -322,7 +322,61 @@ class MainWindow(QMainWindow):
322
322
  if self._try_connect():
323
323
  return True
324
324
 
325
- # ----------------- Tab management ----------------- #
325
+ # ----------------- Tab and date management ----------------- #
326
+
327
+ def _current_date_iso(self) -> str:
328
+ d = self.calendar.selectedDate()
329
+ return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
330
+
331
+ def _date_key(self, qd: QDate) -> tuple[int, int, int]:
332
+ return (qd.year(), qd.month(), qd.day())
333
+
334
+ def _index_for_date_insert(self, date: QDate) -> int:
335
+ """Return the index where a tab for `date` should be inserted (ascending order)."""
336
+ key = self._date_key(date)
337
+ for i in range(self.tab_widget.count()):
338
+ w = self.tab_widget.widget(i)
339
+ d = getattr(w, "current_date", None)
340
+ if isinstance(d, QDate) and d.isValid():
341
+ if self._date_key(d) > key:
342
+ return i
343
+ return self.tab_widget.count()
344
+
345
+ def _reorder_tabs_by_date(self):
346
+ """Reorder existing tabs by their date (ascending)."""
347
+ bar = self.tab_widget.tabBar()
348
+ dated, undated = [], []
349
+
350
+ for i in range(self.tab_widget.count()):
351
+ w = self.tab_widget.widget(i)
352
+ d = getattr(w, "current_date", None)
353
+ if isinstance(d, QDate) and d.isValid():
354
+ dated.append((d, w))
355
+ else:
356
+ undated.append(w)
357
+
358
+ dated.sort(key=lambda t: self._date_key(t[0]))
359
+
360
+ with QSignalBlocker(self.tab_widget):
361
+ # Update labels to yyyy-MM-dd
362
+ for d, w in dated:
363
+ idx = self.tab_widget.indexOf(w)
364
+ if idx != -1:
365
+ self.tab_widget.setTabText(idx, d.toString("yyyy-MM-dd"))
366
+
367
+ # Move dated tabs into target positions 0..len(dated)-1
368
+ for target_pos, (_, w) in enumerate(dated):
369
+ cur = self.tab_widget.indexOf(w)
370
+ if cur != -1 and cur != target_pos:
371
+ bar.moveTab(cur, target_pos)
372
+
373
+ # Keep any undated pages (if they ever exist) after the dated ones
374
+ start = len(dated)
375
+ for offset, w in enumerate(undated):
376
+ cur = self.tab_widget.indexOf(w)
377
+ target = start + offset
378
+ if cur != -1 and cur != target:
379
+ bar.moveTab(cur, target)
326
380
 
327
381
  def _tab_index_for_date(self, date: QDate) -> int:
328
382
  """Return the index of the tab showing `date`, or -1 if none."""
@@ -369,9 +423,7 @@ class MainWindow(QMainWindow):
369
423
  editor.cursorPositionChanged.connect(self._sync_toolbar)
370
424
  editor.textChanged.connect(self._on_text_changed)
371
425
 
372
- # Determine tab title
373
- if date is None:
374
- date = self.calendar.selectedDate()
426
+ # Set tab title
375
427
  tab_title = date.toString("yyyy-MM-dd")
376
428
 
377
429
  # Add the tab
@@ -384,6 +436,12 @@ class MainWindow(QMainWindow):
384
436
  # Store the date with the editor so we can save it later
385
437
  editor.current_date = date
386
438
 
439
+ # Insert at sorted position
440
+ tab_title = date.toString("yyyy-MM-dd")
441
+ pos = self._index_for_date_insert(date)
442
+ index = self.tab_widget.insertTab(pos, editor, tab_title)
443
+ self.tab_widget.setCurrentIndex(index)
444
+
387
445
  return editor
388
446
 
389
447
  def _close_tab(self, index: int):
@@ -425,36 +483,6 @@ class MainWindow(QMainWindow):
425
483
  return
426
484
  getattr(ed, method_name)(*args)
427
485
 
428
- def _bind_toolbar(self):
429
- if getattr(self, "_toolbar_bound", False):
430
- return
431
- tb = self.toolBar
432
-
433
- # keep refs so we never create new lambdas (prevents accidental dupes)
434
- self._tb_bold = lambda: self._call_editor("apply_weight")
435
- self._tb_italic = lambda: self._call_editor("apply_italic")
436
- self._tb_strike = lambda: self._call_editor("apply_strikethrough")
437
- self._tb_code = lambda: self._call_editor("apply_code")
438
- self._tb_heading = lambda level: self._call_editor("apply_heading", level)
439
- self._tb_bullets = lambda: self._call_editor("toggle_bullets")
440
- self._tb_numbers = lambda: self._call_editor("toggle_numbers")
441
- self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
442
-
443
- tb.boldRequested.connect(self._tb_bold)
444
- tb.italicRequested.connect(self._tb_italic)
445
- tb.strikeRequested.connect(self._tb_strike)
446
- tb.codeRequested.connect(self._tb_code)
447
- tb.headingRequested.connect(self._tb_heading)
448
- tb.bulletsRequested.connect(self._tb_bullets)
449
- tb.numbersRequested.connect(self._tb_numbers)
450
- tb.checkboxesRequested.connect(self._tb_checkboxes)
451
-
452
- # these aren’t editor methods
453
- tb.historyRequested.connect(self._open_history)
454
- tb.insertImageRequested.connect(self._on_insert_image)
455
-
456
- self._toolbar_bound = True
457
-
458
486
  def current_editor(self) -> MarkdownEditor | None:
459
487
  """Get the currently active editor."""
460
488
  return self.tab_widget.currentWidget()
@@ -564,6 +592,7 @@ class MainWindow(QMainWindow):
564
592
  if action == open_in_new_tab_action and clicked_date and clicked_date.isValid():
565
593
  self._open_date_in_tab(clicked_date)
566
594
 
595
+ # ----------------- Some theme helpers -------------------#
567
596
  def _retheme_overrides(self):
568
597
  if hasattr(self, "_lock_overlay"):
569
598
  self._lock_overlay._apply_overlay_style()
@@ -698,6 +727,36 @@ class MainWindow(QMainWindow):
698
727
 
699
728
  # --- UI handlers ---------------------------------------------------------
700
729
 
730
+ def _bind_toolbar(self):
731
+ if getattr(self, "_toolbar_bound", False):
732
+ return
733
+ tb = self.toolBar
734
+
735
+ # keep refs so we never create new lambdas (prevents accidental dupes)
736
+ self._tb_bold = lambda: self._call_editor("apply_weight")
737
+ self._tb_italic = lambda: self._call_editor("apply_italic")
738
+ self._tb_strike = lambda: self._call_editor("apply_strikethrough")
739
+ self._tb_code = lambda: self._call_editor("apply_code")
740
+ self._tb_heading = lambda level: self._call_editor("apply_heading", level)
741
+ self._tb_bullets = lambda: self._call_editor("toggle_bullets")
742
+ self._tb_numbers = lambda: self._call_editor("toggle_numbers")
743
+ self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
744
+
745
+ tb.boldRequested.connect(self._tb_bold)
746
+ tb.italicRequested.connect(self._tb_italic)
747
+ tb.strikeRequested.connect(self._tb_strike)
748
+ tb.codeRequested.connect(self._tb_code)
749
+ tb.headingRequested.connect(self._tb_heading)
750
+ tb.bulletsRequested.connect(self._tb_bullets)
751
+ tb.numbersRequested.connect(self._tb_numbers)
752
+ tb.checkboxesRequested.connect(self._tb_checkboxes)
753
+
754
+ # these aren’t editor methods
755
+ tb.historyRequested.connect(self._open_history)
756
+ tb.insertImageRequested.connect(self._on_insert_image)
757
+
758
+ self._toolbar_bound = True
759
+
701
760
  def _sync_toolbar(self):
702
761
  fmt = self.editor.currentCharFormat()
703
762
  c = self.editor.textCursor()
@@ -740,12 +799,8 @@ class MainWindow(QMainWindow):
740
799
  self.toolBar.actBullets.setChecked(bool(bullets_on))
741
800
  self.toolBar.actNumbers.setChecked(bool(numbers_on))
742
801
 
743
- def _current_date_iso(self) -> str:
744
- d = self.calendar.selectedDate()
745
- return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
746
-
747
802
  def _load_selected_date(self, date_iso=False, extra_data=False):
748
- """Load a date into the current editor (backward compatibility)."""
803
+ """Load a date into the current editor"""
749
804
  editor = self.current_editor()
750
805
  if not editor:
751
806
  return
@@ -762,6 +817,9 @@ class MainWindow(QMainWindow):
762
817
  if current_index >= 0:
763
818
  self.tab_widget.setTabText(current_index, date_iso)
764
819
 
820
+ # Keep tabs sorted by date
821
+ self._reorder_tabs_by_date()
822
+
765
823
  def _load_date_into_editor(
766
824
  self, date: QDate, editor: MarkdownEditor, extra_data=False
767
825
  ):
@@ -808,7 +866,7 @@ class MainWindow(QMainWindow):
808
866
  def _adjust_today(self):
809
867
  """Jump to today."""
810
868
  today = QDate.currentDate()
811
- self.calendar.setSelectedDate(today)
869
+ self._create_new_tab(today)
812
870
 
813
871
  def _load_yesterday_todos(self):
814
872
  try:
@@ -888,6 +946,34 @@ class MainWindow(QMainWindow):
888
946
  if current_index >= 0:
889
947
  self.tab_widget.setTabText(current_index, new_date.toString("yyyy-MM-dd"))
890
948
 
949
+ # Keep tabs sorted by date
950
+ self._reorder_tabs_by_date()
951
+
952
+ # ----------- History handler ------------#
953
+ def _open_history(self):
954
+ date_iso = self._current_date_iso()
955
+ dlg = HistoryDialog(self.db, date_iso, self)
956
+ if dlg.exec() == QDialog.Accepted:
957
+ # refresh editor + calendar (head pointer may have changed)
958
+ self._load_selected_date(date_iso)
959
+ self._refresh_calendar_marks()
960
+
961
+ # ----------- Image insert handler ------------#
962
+ def _on_insert_image(self):
963
+ # Let the user pick one or many images
964
+ paths, _ = QFileDialog.getOpenFileNames(
965
+ self,
966
+ "Insert image(s)",
967
+ "",
968
+ "Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
969
+ )
970
+ if not paths:
971
+ return
972
+ # Insert each image
973
+ for path_str in paths:
974
+ self.editor.insert_image_from_path(Path(path_str))
975
+
976
+ # --------------- Database saving of content ---------------- #
891
977
  def _save_date(self, date_iso: str, explicit: bool = False, note: str = "autosave"):
892
978
  """
893
979
  Save editor contents into the given date. Shows status on success.
@@ -937,28 +1023,6 @@ class MainWindow(QMainWindow):
937
1023
  except Exception:
938
1024
  pass
939
1025
 
940
- def _open_history(self):
941
- date_iso = self._current_date_iso()
942
- dlg = HistoryDialog(self.db, date_iso, self)
943
- if dlg.exec() == QDialog.Accepted:
944
- # refresh editor + calendar (head pointer may have changed)
945
- self._load_selected_date(date_iso)
946
- self._refresh_calendar_marks()
947
-
948
- def _on_insert_image(self):
949
- # Let the user pick one or many images
950
- paths, _ = QFileDialog.getOpenFileNames(
951
- self,
952
- "Insert image(s)",
953
- "",
954
- "Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
955
- )
956
- if not paths:
957
- return
958
- # Insert each image
959
- for path_str in paths:
960
- self.editor.insert_image_from_path(Path(path_str))
961
-
962
1026
  # ----------- Settings handler ------------#
963
1027
  def _open_settings(self):
964
1028
  dlg = SettingsDialog(self.cfg, self.db, self)
@@ -1090,7 +1154,7 @@ If you want an encrypted backup, choose Backup instead of Export.
1090
1154
  elif selected_filter.startswith("SQL"):
1091
1155
  self.db.export_sql(filename)
1092
1156
  else:
1093
- self.db.export_by_extension(filename)
1157
+ raise ValueError("Unrecognised extension!")
1094
1158
 
1095
1159
  QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
1096
1160
  except Exception as e:
@@ -112,7 +112,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
112
112
  self.setCurrentBlockState(1 if in_code_block else 0)
113
113
  # Format the fence markers - but keep them somewhat visible for editing
114
114
  # Use code format instead of syntax format so cursor is visible
115
- self.setFormat(0, len(text), self.code_block_format)
115
+ self.setFormat(0, len(text), self.code_format)
116
116
  return
117
117
 
118
118
  if in_code_block:
@@ -258,13 +258,13 @@ class MarkdownEditor(QTextEdit):
258
258
 
259
259
  # Transform only this line:
260
260
  # - "TODO " at start (with optional indent) -> "- ☐ "
261
- # - "- [ ] " -> "- ☐ " and "- [x] " -> "- ☑ "
261
+ # - "- [ ] " -> " ☐ " and "- [x] " -> " ☑ "
262
262
  def transform_line(s: str) -> str:
263
- s = s.replace("- [x] ", f"- {self._CHECK_CHECKED_DISPLAY} ")
264
- s = s.replace("- [ ] ", f"- {self._CHECK_UNCHECKED_DISPLAY} ")
263
+ s = s.replace("- [x] ", f"{self._CHECK_CHECKED_DISPLAY} ")
264
+ s = s.replace("- [ ] ", f"{self._CHECK_UNCHECKED_DISPLAY} ")
265
265
  s = re.sub(
266
266
  r"^([ \t]*)TODO\b[:\-]?\s+",
267
- lambda m: f"{m.group(1)}- {self._CHECK_UNCHECKED_DISPLAY} ",
267
+ lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
268
268
  s,
269
269
  )
270
270
  return s
@@ -293,8 +293,8 @@ class MarkdownEditor(QTextEdit):
293
293
  text = self._extract_images_to_markdown()
294
294
 
295
295
  # Convert Unicode checkboxes back to markdown syntax
296
- text = text.replace(f"- {self._CHECK_CHECKED_DISPLAY} ", "- [x] ")
297
- text = text.replace(f"- {self._CHECK_UNCHECKED_DISPLAY} ", "- [ ] ")
296
+ text = text.replace(f"{self._CHECK_CHECKED_DISPLAY} ", "- [x] ")
297
+ text = text.replace(f"{self._CHECK_UNCHECKED_DISPLAY} ", "- [ ] ")
298
298
 
299
299
  return text
300
300
 
@@ -336,15 +336,15 @@ class MarkdownEditor(QTextEdit):
336
336
  """Load markdown text into the editor (convert markdown checkboxes to Unicode)."""
337
337
  # Convert markdown checkboxes to Unicode for display
338
338
  display_text = markdown_text.replace(
339
- "- [x] ", f"- {self._CHECK_CHECKED_DISPLAY} "
339
+ "- [x] ", f"{self._CHECK_CHECKED_DISPLAY} "
340
340
  )
341
341
  display_text = display_text.replace(
342
- "- [ ] ", f"- {self._CHECK_UNCHECKED_DISPLAY} "
342
+ "- [ ] ", f"{self._CHECK_UNCHECKED_DISPLAY} "
343
343
  )
344
344
  # Also convert any plain 'TODO ' at the start of a line to an unchecked checkbox
345
345
  display_text = re.sub(
346
346
  r"(?m)^([ \t]*)TODO\s",
347
- lambda m: f"{m.group(1)}- {self._CHECK_UNCHECKED_DISPLAY} ",
347
+ lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
348
348
  display_text,
349
349
  )
350
350
 
@@ -425,10 +425,10 @@ class MarkdownEditor(QTextEdit):
425
425
  line = line.lstrip()
426
426
 
427
427
  # Checkbox list (Unicode display format)
428
- if line.startswith(f"- {self._CHECK_UNCHECKED_DISPLAY} ") or line.startswith(
429
- f"- {self._CHECK_CHECKED_DISPLAY} "
428
+ if line.startswith(f"{self._CHECK_UNCHECKED_DISPLAY} ") or line.startswith(
429
+ f"{self._CHECK_CHECKED_DISPLAY} "
430
430
  ):
431
- return ("checkbox", f"- {self._CHECK_UNCHECKED_DISPLAY} ")
431
+ return ("checkbox", f"{self._CHECK_UNCHECKED_DISPLAY} ")
432
432
 
433
433
  # Bullet list
434
434
  if re.match(r"^[-*+]\s", line):
@@ -533,19 +533,19 @@ class MarkdownEditor(QTextEdit):
533
533
 
534
534
  # Check if clicking on a checkbox line
535
535
  if (
536
- f"- {self._CHECK_UNCHECKED_DISPLAY} " in line
537
- or f"- {self._CHECK_CHECKED_DISPLAY} " in line
536
+ f"{self._CHECK_UNCHECKED_DISPLAY} " in line
537
+ or f"{self._CHECK_CHECKED_DISPLAY} " in line
538
538
  ):
539
539
  # Toggle the checkbox
540
- if f"- {self._CHECK_UNCHECKED_DISPLAY} " in line:
540
+ if f"{self._CHECK_UNCHECKED_DISPLAY} " in line:
541
541
  new_line = line.replace(
542
- f"- {self._CHECK_UNCHECKED_DISPLAY} ",
543
- f"- {self._CHECK_CHECKED_DISPLAY} ",
542
+ f"{self._CHECK_UNCHECKED_DISPLAY} ",
543
+ f"{self._CHECK_CHECKED_DISPLAY} ",
544
544
  )
545
545
  else:
546
546
  new_line = line.replace(
547
- f"- {self._CHECK_CHECKED_DISPLAY} ",
548
- f"- {self._CHECK_UNCHECKED_DISPLAY} ",
547
+ f"{self._CHECK_CHECKED_DISPLAY} ",
548
+ f"{self._CHECK_UNCHECKED_DISPLAY} ",
549
549
  )
550
550
 
551
551
  cursor.insertText(new_line)
@@ -745,18 +745,18 @@ class MarkdownEditor(QTextEdit):
745
745
 
746
746
  # Check if already has checkbox (Unicode display format)
747
747
  if (
748
- f"- {self._CHECK_UNCHECKED_DISPLAY} " in line
749
- or f"- {self._CHECK_CHECKED_DISPLAY} " in line
748
+ f"{self._CHECK_UNCHECKED_DISPLAY} " in line
749
+ or f"{self._CHECK_CHECKED_DISPLAY} " in line
750
750
  ):
751
751
  # Remove checkbox - use raw string to avoid escape sequence warning
752
752
  new_line = re.sub(
753
- rf"^\s*-\s*[{self._CHECK_UNCHECKED_DISPLAY}{self._CHECK_CHECKED_DISPLAY}]\s+",
753
+ rf"^\s*[{self._CHECK_UNCHECKED_DISPLAY}{self._CHECK_CHECKED_DISPLAY}]\s+",
754
754
  "",
755
755
  line,
756
756
  )
757
757
  else:
758
758
  # Add checkbox (Unicode display format)
759
- new_line = f"- {self._CHECK_UNCHECKED_DISPLAY} " + line.lstrip()
759
+ new_line = f"{self._CHECK_UNCHECKED_DISPLAY} " + line.lstrip()
760
760
 
761
761
  cursor.insertText(new_line)
762
762
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bouquin"
3
- version = "0.2.1"
3
+ version = "0.2.1.2"
4
4
  description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
5
5
  authors = ["Miguel Jacq <mig@mig5.net>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes