Compare commits

..

146 Commits

Author SHA1 Message Date
c204e4e094 Bump liberty and termo
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2025-04-23 22:00:17 +02:00
16e0ff2188 Make README.adoc a bit less confusing
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-19 15:29:58 +01:00
2d6855445f Update the screenshot
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-19 15:24:53 +01:00
531f18d827 GUI: add basic configuration
It is simply not feasible to write the text file by hand on Windows.
2024-12-19 14:38:19 +01:00
862cde36ae CMakeLists.txt: quote more paths 2024-12-07 22:50:53 +01:00
661dc85d45 Fix macOS build
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
2024-12-04 18:10:35 +01:00
2fe846f09f Fix running tests in Windows builds
All checks were successful
Arch Linux AUR Success
Alpine 3.19 Success
2024-04-10 12:48:05 +02:00
33426992ec Bump liberty, moving the Win64 toolchain file
All checks were successful
Arch Linux AUR Success
2024-04-09 17:04:45 +02:00
197d071160 Enable cross-compiled tests
All checks were successful
Arch Linux AUR Success
2024-04-09 13:04:32 +02:00
fafac22d60 Bump liberty
All checks were successful
Arch Linux AUR Success
2024-02-10 05:46:13 +01:00
58f7ba55b3 CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:09:31 +02:00
d2cfc2ee81 Deduplicate CMake scripts 2023-07-24 11:47:33 +02:00
5a9a446b9c Fix build on OpenIndiana 2023-07-24 08:24:52 +02:00
39e2fc5142 Find ncursesw on OpenIndiana 2023-07-24 08:07:26 +02:00
f94bb77091 Reflect the recent renaming of tabfile in scripts 2023-07-07 12:15:37 +02:00
d3cfb12e16 README.adoc: update package information 2023-07-01 22:00:54 +02:00
9aac2511d3 Win64Depends.sh: only extract what we need
In case the packages directory has been preloaded or symlinked.
2023-06-26 20:57:56 +02:00
8af337c83c CMakeLists.txt: fix dependencies after renaming 2023-06-22 00:23:29 +02:00
951208c15b Test SVG and desktop file validity 2023-06-15 16:24:16 +02:00
74d9acecb5 Bump termo 2023-06-14 16:24:27 +02:00
d1ce97010e Update the manual page 2023-06-11 18:14:46 +02:00
e7be281b58 Reflect upstream URL rename 2023-06-11 18:12:12 +02:00
2aa6390146 Update translations 2023-06-11 18:08:03 +02:00
c77d994dc4 Rename tools, make them installable 2023-06-11 18:08:03 +02:00
238e7a2bb9 Merge TUI and GUI binaries, using a new name
The appropriate interface will be chosen automatically.
2023-06-11 18:08:03 +02:00
7bcbc04b04 Bump liberty
This improves the still-imperfect fallback manpage output.
2023-06-11 10:11:54 +02:00
59d7c4af17 Improve manpage AsciiDoc compatibility 2023-06-10 15:05:12 +02:00
2ed1c005c9 Import liberty for its manpage generator
It cannot parse the page well, so it's just a convenience fallback.
2023-06-10 12:12:42 +02:00
26e73711b1 Improve GLib version compatibility 2023-06-10 12:12:41 +02:00
d13b4a793d Prevent undefined behaviour 2022-09-24 18:42:59 +02:00
0570a4d050 sdtui: measure the "search" prompt properly 2022-09-24 18:42:30 +02:00
e28e576fdb sdtui: improve tab bar overflow behaviour 2022-09-24 11:15:25 +02:00
ae9952387a sdgui: don't highlight when cursor not on widget 2022-09-12 04:00:26 +02:00
27dcf87a64 sdgui: load dictionaries asynchronously
This is a must when loading huge dictionaries,
where not even parallelization helps much.
2022-09-04 13:16:40 +02:00
7ef502759e sdgui: clean up DnD 2022-09-04 12:03:26 +02:00
ded899933c sdgui: make Ctrl+click put hovered word in search 2022-09-03 22:00:53 +02:00
832842bf81 sdgui: don't reload on size-allocate
We could annoyingly get these events on window de/focus.
2022-09-03 18:17:04 +02:00
49072f9d01 sdgui: fix context menu item sensitivity 2022-09-03 17:19:12 +02:00
b0f1d3d6ea Build with AsciiDoc as well as Asciidoctor
And bump copyright years.
2022-08-24 00:55:32 +02:00
4073749d3b Synchronize cross-compilation scripts 2022-08-11 14:15:20 +02:00
2e684d2f4e Clean up cross-compilation 2022-08-10 16:04:10 +02:00
57739ff81e Update README 2022-08-05 00:08:54 +02:00
2ff01f9fdb sdgui: support text selection in the view
This is generally an improvement over the initial GtkLabel approach:
 - Multiple definition lines can be selected at once.
 - The widget doesn't keep a selection caret around (which means
   it can't be controlled from the keyboard, a conscious trade-off).
 - Text doesn't needlessly go to PRIMARY immediately during selection,
   making it somewhat possible lift the self-exception for
   the PRIMARY selection watch.

Closes #2
2022-08-04 05:23:09 +02:00
5ed881d25b czech-wordnet.pl: improve portability
Perl runs `` commands in the shell only if they contain
shell metacharacters.
2022-08-01 23:25:15 +02:00
259d0ac860 sdgui: support touch screen dragging for the view 2022-07-24 23:15:18 +02:00
aa985514a6 sdgui: fix up the smooth scrolling commit 2022-07-24 23:15:18 +02:00
6f5e32386e tabfile: fix mismatching popen/fclose 2022-07-24 23:15:18 +02:00
92556d5269 README: mention gettext utilities as a dependency 2022-07-24 23:15:10 +02:00
2c69937ef5 sdgui: use smooth scrolling 2022-07-24 21:36:57 +02:00
4e4ba67025 README: mention icoutils 2022-07-24 21:15:24 +02:00
13ddec0274 Add a missing gtk_drag_finish() call 2022-02-17 11:02:35 +01:00
c899ceff10 sdgui: avoid focusing tab headers by mouse
Our tabs have dummy contents, which causes some complications.
2022-02-11 08:20:10 +01:00
4d95f46d36 Bump termo 2021-11-04 14:23:40 +01:00
0d0ac40f96 sdtui: handle input field overflows reasonably 2021-11-04 11:43:18 +01:00
181df7fbae czech-wordnet.pl: fix to work on non-GNU systems 2021-11-04 08:33:28 +01:00
b36f185426 Fix various macOS-related/found issues
Even though this software isn't very useful on that system either,
due to its lack of PRIMARY.
2021-11-02 17:04:37 +01:00
726ecd83ac Win64Depends.sh: describe a more precise fix 2021-11-01 09:28:18 +01:00
dd5e90a324 CMakeLists.txt: update icon cache after cleanup
docs/iconcache.txt says the format is machine-independent.

It does not seem to result in any noticeable improvement.
2021-11-01 08:42:41 +01:00
ac6ac4f845 Win32CleanupAdwaita.sh: make note of a corner case
I've actually tried to replicate that behaviour earlier,
and it kept around way too many icons.
2021-11-01 02:40:41 +01:00
093baaa034 sdgui: make M-Tab go to the last chosen dictionary 2021-10-30 10:45:32 +02:00
ecab86966f sdtui: make M-Tab go to the last chosen dictionary 2021-10-30 10:18:20 +02:00
e383a50af7 Add clang-format configuration, clean up 2021-10-30 03:02:33 +02:00
ed26259e6d sdgui: fix M-0 going one item beyond our intent 2021-10-28 21:42:49 +02:00
8c80aa9da2 CMakeLists.txt: configure NSIS
- Install a link to sdgui to the Start Menu.
 - Associate the .ifo file extension with sdgui.
 - Change the installation directory name to contain a space,
   rather than a dash, since that appears to be the norm.
   It's also copied over to the display name.

And thus, the GUI has become somewhat usable on Windows.
2021-10-27 22:52:29 +02:00
9aa26e2807 CMakeLists.txt: think of older versions
At minimum it documents the novelty of the command argument.
2021-10-27 19:54:02 +02:00
f7528a05a3 CMakeLists.txt: refactor icon generation 2021-10-27 19:23:47 +02:00
46e9b7b584 sdgui: rasterize some icons, including for Windows 2021-10-27 09:48:57 +02:00
0eb935ad9f sdgui: set the window title explicitly 2021-10-27 09:46:54 +02:00
c75de30765 sdgui: create and install a program icon 2021-10-27 09:46:17 +02:00
451859e976 Fix the test target and unargumented ctest
CMake does know how to be tough on users, but we've won at last.
2021-10-27 00:18:01 +02:00
0f45b9bf3b Test the project's XMLs for well-formedness
But only if the respective binaries can be found.
2021-10-26 23:34:04 +02:00
27a9869a6a Trim down the Windows build a bit
Remove most unused Adwaita icons and cursors, it's about 30 mebibytes
of raw bullshit that doesn't seem to be missing in any shape or form.

Rather sadly, the script is too complex to be rewritten in CMake script.
2021-10-26 11:31:50 +02:00
2efad7453a sdgui: let the view act as a drop target for files
Inspired by how the only reasonable method of launching sdgui
on Windows is by using drag and drop on the executable.

Sadly the top-level window cannot be used as a whole for this.
2021-10-25 04:02:37 +02:00
dc36a88968 Improve GLib deprecation disabling
Added a match against the version stated in the README.
2021-10-24 16:30:44 +02:00
0371dd95dd sdgui: fix popup menu alignment
On Windows 10 GTK+ completely failed to turn it around
so that it would be visible.
2021-10-23 18:51:23 +02:00
cbdb1cfaa6 Remove unnecessary DLLs from Windows builds
This saves 21 MiB of MSYS2 libraries, without any adverse effects.

The MSYS2 build remains bloated, due to the Adwaita icon theme.
2021-10-23 18:27:43 +02:00
03ebaddff5 sdgui: add a file open dialog to the menu
And remove the selection following checkbox from Windows builds.
2021-10-23 02:12:24 +02:00
5190601852 sdgui: make C-Page Up/Down wrap around
To mimic sdtui behaviour.
2021-10-23 00:45:29 +02:00
b77395b931 make-template.sh: minor improvements
Normalize the locale, and be more friendly towards paths with spaces.
2021-10-22 23:32:56 +02:00
809304cbb3 README: fix cross-building instructions
Copy-pasting and mental exhaustion do not go well together.
2021-10-22 23:32:52 +02:00
462428d0a2 sdgui: the firstclassing continues 2021-10-22 02:20:04 +02:00
89580f2113 sdgui: cross-compile for Windows
No one bothered to ask whether it /should/ be done.

The hamburger needs to be replaced with a file open dialog there.
2021-10-22 01:59:09 +02:00
c7b9d65797 README: make sdgui even more 1st-class
It has reached a fairly high level of usability already.
2021-10-20 14:01:46 +02:00
519d6bd108 sdgui: bind Up/Down to view scrolling 2021-10-20 11:51:38 +02:00
b721e26557 sdgui: improve styling of the text entry 2021-10-20 11:46:52 +02:00
ec89870a32 sdgui: trim selection text
Just like sdtui does.
2021-10-20 11:19:47 +02:00
6158f6e3b5 sdgui: avoid Pango markup for keywords
Sadly, there is no way to make this an improvement for the end user.
2021-10-20 11:17:41 +02:00
dd7b258698 sdgui: make an attempt at smooth scrolling 2021-10-20 09:46:06 +02:00
aa3ad12d44 sdgui: decrease source code line count 2021-10-20 09:17:53 +02:00
82accaf200 sdgui: make Page Up/Down scroll the view 2021-10-20 09:15:06 +02:00
e461189f0e sdgui: ignore Caps Lock for accelerators 2021-10-20 08:58:53 +02:00
85a30d20c3 sdgui: accelerate the hamburger
The hamburger initially resisted being accelerated.
2021-10-20 08:35:00 +02:00
54ef836eec sdgui: make even rows a bit darker by default 2021-10-17 12:46:06 +02:00
26a3c0c825 sdgui: bind ^W as in Readline 2021-10-17 11:58:06 +02:00
5216205056 README: make sdgui more of a first-class citizen 2021-10-17 11:25:32 +02:00
c364ec3b81 sdgui: stop hardcoding cell side padding 2021-10-17 11:18:29 +02:00
a31d329754 sdgui: stop hardcoding colours
Reusing colours from sdtui configuration would be awkward
and complicated, e.g. with font attributes, so abandon that idea.
2021-10-17 09:37:32 +02:00
33e98881ad sdgui: add a MIME-associated desktop file
Neither StarDict nor shared-mime-info have their own MIME DB file.
2021-10-16 09:00:31 +02:00
c0a094e473 sdgui: load dictionaries in parallel, as sdtui did
Also, resolve some use-after-frees in GTK+.
2021-10-16 08:34:37 +02:00
f147b54393 sdgui: load dictionaries from sdtui configuration 2021-10-16 06:46:44 +02:00
10c05a2422 sdgui: clean-up
It doesn't seem like we'll want to remember the position.
2021-10-16 03:45:38 +02:00
8fb2ce29cf sdgui: clean up scrolling code 2021-10-15 23:59:45 +02:00
ce92a23551 sdgui: scroll by three rows
Don't use an arbitrary amount of pixels, base it off the font.
2021-10-15 23:09:26 +02:00
573554b9de sdgtk -> sdgui, improve build, mention in README
It's finally not horrible.
2021-10-15 12:09:37 +02:00
9d7bc2a839 sdgtk: add and use a custom listview widget
Nothing in GTK+ appears to be suited for what are virtually infinite
lists.  Our workaround with GtkLabel and GtkScrolledWindow has been
heavily suboptimal and needs to be replaced.

Use Pango directly to handle our relatively simple needs.

Upgrades:
 - the widget can be scrolled,
 - keywords are repeated for each definition line,
 - definition lines are now wrapped, and support 'g' and 'x' fields.

Downgrades:
 - text can no longer be selected, so far.
2021-10-15 11:46:09 +02:00
f812fae922 Add the GNU/FDL German-Czech dictionary to dicts
But only build it with WANT_BAD_DICTS set to non-null.
2021-10-13 23:58:57 +02:00
3d53b2c131 Fix g_option_context_get_help() usage 2021-10-12 02:39:16 +02:00
627c296057 query-tool: support more field types
Add options to format the output for the terminal, or IRC messages.

Changed the output format to separate dictionary name with a tab,
so it's now rather similar to tabfiles.
2021-10-12 02:38:40 +02:00
13a16d1eb5 Update translations 2021-10-12 02:02:40 +02:00
b8e43c5d5a make-template.sh: fix, improve, update, run 2021-10-11 21:15:11 +02:00
e2790b42f3 sdtui: fix introductory message centring 2021-10-11 21:15:11 +02:00
9b220b74cc sdtui: minor rendering clean-up 2021-10-11 21:15:11 +02:00
6f569e076e sdtui: further improve the XML mangler
Comments should be more or less reliably handled by GMarkup now.
2021-10-11 06:32:00 +02:00
ce2b8b39c0 Update README
We're not that all that far away from a stable version.
2021-10-11 02:46:56 +02:00
e57751fe0e sdtui: skip keywords in XDXF 2021-10-11 02:43:34 +02:00
4d6cd247cb sdtui: implement elementary XDXF display
We're lacking word wrapping, but it's more or less usable.
2021-10-11 02:43:30 +02:00
b9ba894cc9 Replace the screenshot
To make it apparent what this program is good for.
2021-10-10 07:23:44 +02:00
de7089d669 gnu-fdl-en-cz.sh: employ Pango formatting
Also add collation fields.
2021-10-10 06:13:49 +02:00
16d6eaf012 sdtui: support <b>/<i>/<u> in Pango entries 2021-10-10 06:13:49 +02:00
bc939712cb sdtui: lay the groundwork for formatted entries 2021-10-10 06:13:49 +02:00
6f7fbbb438 sdtui: keep the per-ViewEntry GPtrArrays 2021-10-10 06:13:48 +02:00
39ff4069c0 tabfile: fix file format version downgrade 2021-10-10 01:57:51 +02:00
fbfd8c7d02 tabfile: add support for writing Pango markup
Enabling this option will ensure the field would parse.
2021-10-10 01:52:15 +02:00
8b9c5e0460 Add the Czech WordNet snapshot to dicts 2021-10-10 00:45:38 +02:00
973d1d27ea Improve documentation 2021-10-07 20:11:36 +02:00
55d0f53f7a Abandon the idea of sts for slovnik-cizich-slov
We'd need something like inlining the type sequence within the data,
which doesn't seem particularly elegant.
2021-10-07 17:24:24 +02:00
20fcf2a0c7 tabfile: make it possible to set metadata
And some related clean-up.
2021-10-07 16:28:44 +02:00
ed8b1bcdad Add sample dictionary downloaders/builders 2021-10-07 14:06:57 +02:00
3881725904 tabfile: remember to check UTF-8, glibize 2021-10-06 22:11:21 +02:00
6c364dc997 Add an implementation of tabfile
The original one is a horrible thing.  Now we're self-reliant.
2021-10-06 22:04:55 +02:00
690402f2e1 Unbreak stardict_dict_get_synonyms()
Untested, but certainly broken six ways to Sunday before.
2021-10-06 20:14:49 +02:00
03f2123447 GLib-related improvements
Now all error messages produced by tools should be in the right
encoding, even if the system isn't in UTF-8.
2021-10-06 16:11:55 +02:00
3c87b95c31 Make Tab put the current definition into search
Moving some X11-only code out.
2021-10-06 13:44:54 +02:00
fffa2906d8 Update README
It turns out sdtui works in Sway out of the box (essentially).
2021-07-24 23:46:28 +02:00
4245dc35df Update README, add a screenshot
I've given up on the idea of a "proper" TUI framework.

I've also learnt that Wayland isn't a completely dead end.
2021-07-23 20:46:14 +02:00
87f90f6420 Update .gitignore 2021-07-07 00:02:00 +02:00
2b5eb86a9f sdtui: enable styling of defocused selected rows
The defaults are unaffected, the row is always reverse, like it used to.

Having the deselected row just be underlined seems sensible.

It isn't currently possible to change /just/ the foreground or
the background colour of the selection, due to how ncurses works
with colours.

Bumped termo to enable requesting the appropriate events.
2021-07-03 11:44:01 +02:00
85ca0c5857 sdtui: normalize whitespace in clipboard input 2021-07-03 11:36:46 +02:00
bb4e732a25 Bump termo 2020-10-29 15:57:14 +01:00
911749475e CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 15:43:05 +01:00
f15fc0f00a Cleanup 2020-10-26 17:57:24 +01:00
d2fa9f3151 Convert the manpage to AsciiDoc
Writing DocBook XML by hand is an awful experience and the tools aren't
much better.  Asciidoctor does it well.  There's no need to worry about
semantics, man(1) just needs to be able to show something at all.

This project's manpage is sadly almost useless right now.
2020-10-26 17:06:13 +01:00
d7f502a731 Bump termo 2020-10-26 15:16:47 +01:00
7743e21bca Bump minimum CMake version to 3.0
A nice, round number.  This allows us to remove some boilerplate.
2020-10-26 15:16:46 +01:00
504f1ce2f5 Bump termo 2020-09-14 18:07:12 +02:00
e47667b28a sdgtk: don't follow our own selection
When the main window is focused, we are probably the owner of it.
2020-09-14 02:02:38 +02:00
43 changed files with 4803 additions and 1627 deletions

32
.clang-format Normal file
View File

@@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignEscapedNewlines: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
MaxEmptyLinesToKeep: 2
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# Must be kept synchronized with gi18n.h and make-template.sh.
WhitespaceSensitiveMacros: ['_', 'Q_', 'N_', 'C_', 'NC_']
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2

10
.gitignore vendored
View File

@@ -3,7 +3,9 @@
# Qt Creator files
/CMakeLists.txt.user*
/sdtui.config
/sdtui.files
/sdtui.creator*
/sdtui.includes
/tdv.cflags
/tdv.cxxflags
/tdv.config
/tdv.files
/tdv.creator*
/tdv.includes

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "termo"]
path = termo
url = https://git.janouch.name/p/termo.git
[submodule "liberty"]
path = liberty
url = https://git.janouch.name/p/liberty.git

View File

@@ -1,37 +1,55 @@
project (sdtui C)
cmake_minimum_required (VERSION 2.8.5)
cmake_minimum_required (VERSION 3.0...3.27)
project (tdv VERSION 0.1.0 LANGUAGES C)
# Moar warnings
# Adjust warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99")
set (CMAKE_C_FLAGS_DEBUG
"${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wno-missing-field-initializers")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (ignores "-Wno-missing-field-initializers -Wno-cast-function-type")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 ${ignores}")
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra")
endif ()
# Version
set (project_VERSION_MAJOR "0")
set (project_VERSION_MINOR "1")
set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}")
set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}")
add_definitions (-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_38)
# For custom modules
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
set (CMAKE_MODULE_PATH
"${PROJECT_SOURCE_DIR}/cmake;${PROJECT_SOURCE_DIR}/liberty/cmake")
# Cross-compilation for Windows, as a proof-of-concept pulled in from logdiag
if (WIN32)
if (NOT CMAKE_CROSSCOMPILING)
message (FATAL_ERROR "Win32 must be cross-compiled to build sensibly")
endif ()
set (win32_deps_root "${PROJECT_SOURCE_DIR}")
set (win32_deps_prefix "${win32_deps_root}/mingw64")
list (APPEND CMAKE_PREFIX_PATH "${win32_deps_prefix}")
list (APPEND CMAKE_INCLUDE_PATH "${win32_deps_prefix}/lib")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mms-bitfields")
if (CMAKE_CROSSCOMPILING)
list (APPEND CMAKE_FIND_ROOT_PATH ${win32_deps_prefix})
endif ()
# Relativize prefixes, and bar pkg-config from looking up host libraries
set (ENV{PKG_CONFIG_SYSROOT_DIR} "${win32_deps_root}")
set (win32_deps_pcpath
"${win32_deps_prefix}/share/pkgconfig:${win32_deps_prefix}/lib/pkgconfig")
set (ENV{PKG_CONFIG_PATH} "${win32_deps_pcpath}")
set (ENV{PKG_CONFIG_LIBDIR} "${win32_deps_pcpath}")
endif ()
# Dependencies
find_package (ZLIB REQUIRED)
find_package (Ncursesw REQUIRED)
find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED glib-2.0 gio-2.0 pango)
pkg_check_modules (dependencies REQUIRED glib-2.0>=2.38 gio-2.0 pango)
pkg_check_modules (icu icu-uc icu-i18n)
if (NOT icu_FOUND)
if (NOT icu_FOUND AND NOT WIN32)
find_program (icu_CONFIG_EXECUTABLE icu-config)
if (NOT icu_CONFIG_EXECUTABLE)
message (FATAL_ERROR "ICU not found")
endif (NOT icu_CONFIG_EXECUTABLE)
endif ()
execute_process (COMMAND ${icu_CONFIG_EXECUTABLE} --cppflags
OUTPUT_VARIABLE icu_CPPFLAGS OUTPUT_STRIP_TRAILING_WHITESPACE)
@@ -47,24 +65,25 @@ if (NOT icu_FOUND)
foreach (flag ${icu_CPPFLAGS})
if (flag MATCHES "^-I(.*)")
list (APPEND icu_INCLUDE_DIRS "${CMAKE_MATCH_1}")
endif (flag MATCHES "^-I(.*)")
endforeach (flag)
endif ()
endforeach ()
# This should suffice most of the time, don't care about the rest
endif (NOT icu_FOUND)
endif ()
find_package (Termo QUIET NO_MODULE)
option (USE_SYSTEM_TERMO
"Don't compile our own termo library, use the system one" ${Termo_FOUND})
if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found")
endif (NOT Termo_FOUND)
else (USE_SYSTEM_TERMO)
# We don't want the library to install, even though EXCLUDE_FROM_ALL
# sabotages CTest -- those unbuilt tests need to be excluded in CTest runs
endif ()
elseif (NOT WIN32)
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
add_subdirectory (termo EXCLUDE_FROM_ALL)
file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
"execute_process (COMMAND ${CMAKE_COMMAND} --build termo)")
# We don't have many good choices; this is a relatively clean approach
# (other possibilities: setting a variable in the parent scope, using
# a cache variable, writing a special config file with build paths in it
@@ -72,22 +91,31 @@ else (USE_SYSTEM_TERMO)
get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static)
endif (USE_SYSTEM_TERMO)
endif ()
pkg_check_modules (xcb xcb xcb-xfixes)
option (WITH_X11 "Compile with X11 selection support using XCB" ${xcb_FOUND})
if (WITH_X11)
if (NOT xcb_FOUND)
message (FATAL_ERROR "XCB not found")
endif (NOT xcb_FOUND)
endif ()
list (APPEND dependencies_INCLUDE_DIRS ${xcb_INCLUDE_DIRS})
list (APPEND dependencies_LIBRARY_DIRS ${xcb_LIBRARY_DIRS})
list (APPEND dependencies_LIBRARIES ${xcb_LIBRARIES})
endif (WITH_X11)
include_directories (${xcb_INCLUDE_DIRS})
link_directories (${xcb_LIBRARY_DIRS})
endif ()
link_directories (${dependencies_LIBRARY_DIRS})
pkg_check_modules (gtk gtk+-3.0)
option (WITH_GUI "Build an alternative GTK+ UI" ${gtk_FOUND})
if (WITH_GUI)
if (NOT gtk_FOUND)
message (FATAL_ERROR "GTK+ not found")
endif ()
include_directories (${gtk_INCLUDE_DIRS})
link_directories (${gtk_LIBRARY_DIRS})
endif ()
link_directories (${dependencies_LIBRARY_DIRS} ${icu_LIBRARY_DIRS})
include_directories (${ZLIB_INCLUDE_DIRS} ${icu_INCLUDE_DIRS}
${dependencies_INCLUDE_DIRS} ${Ncursesw_INCLUDE_DIRS}
${Termo_INCLUDE_DIRS})
@@ -99,146 +127,296 @@ CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
# Localization
find_package (Gettext REQUIRED)
file (GLOB project_PO_FILES ${PROJECT_SOURCE_DIR}/po/*.po)
file (GLOB project_PO_FILES "${PROJECT_SOURCE_DIR}/po/*.po")
GETTEXT_CREATE_TRANSLATIONS (
${PROJECT_SOURCE_DIR}/po/${PROJECT_NAME}.pot
"${PROJECT_SOURCE_DIR}/po/${PROJECT_NAME}.pot"
ALL ${project_PO_FILES})
# Documentation
find_program (XSLTPROC_EXECUTABLE xsltproc)
if (NOT XSLTPROC_EXECUTABLE)
message (FATAL_ERROR "xsltproc not found")
endif (NOT XSLTPROC_EXECUTABLE)
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
find_program (A2X_EXECUTABLE a2x)
if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
message (WARNING "Neither asciidoctor nor a2x were found, "
"falling back to a substandard manual page generator")
endif ()
set (project_MAN_PAGES "${PROJECT_NAME}.1")
foreach (page ${project_MAN_PAGES})
foreach (page "${PROJECT_NAME}.1")
set (page_output "${PROJECT_BINARY_DIR}/${page}")
list (APPEND project_MAN_PAGES_OUTPUT "${page_output}")
add_custom_command (OUTPUT ${page_output}
COMMAND ${XSLTPROC_EXECUTABLE}
--nonet
--param make.year.ranges 1
--param make.single.year.ranges 1
--param man.charmap.use.subset 0
--param man.authors.section.enabled 0
http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl
"${PROJECT_SOURCE_DIR}/docs/${page}.xml"
DEPENDS "docs/${page}.xml"
COMMENT "Generating man page for ${page}" VERBATIM)
endforeach (page)
list (APPEND project_MAN_PAGES "${page_output}")
if (ASCIIDOCTOR_EXECUTABLE)
add_custom_command (OUTPUT "${page_output}"
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
-a release-version=${PROJECT_VERSION}
-o "${page_output}"
"${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
DEPENDS "docs/${page}.adoc"
COMMENT "Generating man page for ${page}" VERBATIM)
elseif (A2X_EXECUTABLE)
add_custom_command (OUTPUT "${page_output}"
COMMAND ${A2X_EXECUTABLE} --doctype manpage --format manpage
-a release-version=${PROJECT_VERSION}
-D "${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
DEPENDS "docs/${page}.adoc"
COMMENT "Generating man page for ${page}" VERBATIM)
else ()
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk)
add_custom_command (OUTPUT "${page_output}"
COMMAND env LC_ALL=C asciidoc-release-version=${PROJECT_VERSION}
awk -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
> "${page_output}"
DEPENDS "docs/${page}.adoc" ${ASCIIMAN}
COMMENT "Generating man page for ${page}" VERBATIM)
endif ()
endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES_OUTPUT})
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
# Project libraries
set (project_common_libraries ${ZLIB_LIBRARIES} ${icu_LIBRARIES}
${dependencies_LIBRARIES})
if (WIN32)
find_package (LibIntl REQUIRED)
list (APPEND project_common_libraries ${LibIntl_LIBRARIES})
endif (WIN32)
# Project source files
set (project_common_sources
src/dictzip-input-stream.c
src/generator.c
src/stardict.c
src/utils.c)
set (project_common_headers
${PROJECT_BINARY_DIR}/config.h
"${PROJECT_BINARY_DIR}/config.h"
src/dictzip-input-stream.h
src/stardict.h
src/stardict-private.h
src/generator.h
src/utils.h)
# Project libraries
set (project_common_libraries ${ZLIB_LIBRARIES} ${icu_LIBRARIES}
${dependencies_LIBRARIES} ${Ncursesw_LIBRARIES} termo-static)
# Create a common project library so that source files are only compiled once
if (${CMAKE_VERSION} VERSION_GREATER "2.8.7")
add_library (stardict OBJECT
${project_common_sources}
${project_common_headers})
set (project_common_sources $<TARGET_OBJECTS:stardict>)
else (${CMAKE_VERSION} VERSION_GREATER "2.8.7")
add_library (stardict STATIC
${project_common_sources}
${project_common_headers})
target_link_libraries (stardict ${project_common_libraries})
list (APPEND project_common_libraries stardict)
set (project_common_sources)
endif (${CMAKE_VERSION} VERSION_GREATER "2.8.7")
add_library (stardict OBJECT
${project_common_headers}
src/dictzip-input-stream.c
src/generator.c
src/stardict.c
src/utils.c)
set (project_common_sources $<TARGET_OBJECTS:stardict>)
# Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h)
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
configure_file ("${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h")
include_directories ("${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}")
# Primary target source files
# Build the main executable and link it
set (project_libraries
${project_common_libraries})
set (project_sources
${project_common_sources}
src/${PROJECT_NAME}.c)
set (project_headers
${project_common_headers})
# Build the main executable and link it
add_definitions (-DGLIB_DISABLE_DEPRECATION_WARNINGS)
add_executable (${PROJECT_NAME}
${project_sources} ${project_headers} ${project_common_sources})
target_link_libraries (${PROJECT_NAME} ${project_common_libraries})
if (WITH_GUI)
include (IconUtils)
# Experimental GTK+ frontend, we link it with ncurses but we don't care
pkg_check_modules (gtk gtk+-3.0)
if (gtk_FOUND)
add_executable (sdgtk EXCLUDE_FROM_ALL
src/sdgtk.c ${project_common_sources})
target_include_directories (sdgtk PUBLIC ${gtk_INCLUDE_DIRS})
target_link_libraries (sdgtk ${gtk_LIBRARIES} ${project_common_libraries})
endif (gtk_FOUND)
# The largest size is mainly for an appropriately sized Windows icon
set (icon_base "${PROJECT_BINARY_DIR}/icons")
set (icon_png_list)
foreach (icon_size 16 32 48 256)
icon_to_png (${PROJECT_NAME} "${PROJECT_SOURCE_DIR}/${PROJECT_NAME}.svg"
${icon_size} "${icon_base}" icon_png)
list (APPEND icon_png_list "${icon_png}")
endforeach ()
add_custom_target (icons ALL DEPENDS ${icon_png_list})
endif ()
if (WIN32)
list (REMOVE_ITEM icon_png_list "${icon_png}")
set (icon_ico "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ico")
icon_for_win32 ("${icon_ico}" "${icon_png_list}" "${icon_png}")
set (resource_file "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.rc")
list (APPEND project_sources "${resource_file}")
add_custom_command (OUTPUT "${resource_file}"
COMMAND ${CMAKE_COMMAND} -E echo "1 ICON \"${PROJECT_NAME}.ico\""
> "${resource_file}" VERBATIM)
set_property (SOURCE "${resource_file}"
APPEND PROPERTY OBJECT_DEPENDS "${icon_ico}")
else ()
list (APPEND project_libraries ${Ncursesw_LIBRARIES} ${Termo_LIBRARIES})
list (APPEND project_sources
src/${PROJECT_NAME}-tui.c)
endif ()
if (WITH_X11)
list (APPEND project_libraries ${xcb_LIBRARIES})
endif ()
if (WITH_GUI)
list (APPEND project_libraries ${gtk_LIBRARIES})
list (APPEND project_sources
src/${PROJECT_NAME}-gui.c
src/stardict-view.c)
add_executable (${PROJECT_NAME} WIN32 ${project_sources} ${project_headers})
else ()
add_executable (${PROJECT_NAME} ${project_sources} ${project_headers})
endif ()
target_link_libraries (${PROJECT_NAME} ${project_libraries})
# Tools
set (tools add-pronunciation query-tool transform)
set (tools tdv-tabfile tdv-add-pronunciation tdv-query-tool tdv-transform)
foreach (tool ${tools})
add_executable (${tool} EXCLUDE_FROM_ALL
src/${tool}.c ${project_common_sources})
target_link_libraries (${tool} ${project_common_libraries})
endforeach (tool)
endforeach ()
add_custom_target (tools DEPENDS ${tools})
option (WITH_TOOLS "Build and install some StarDict tools" ${UNIX})
if (WITH_TOOLS)
add_custom_target (tools ALL DEPENDS ${tools})
endif ()
# Example dictionaries
file (GLOB dicts_scripts "${PROJECT_SOURCE_DIR}/dicts/*.*")
set (dicts_targets)
foreach (dict_script ${dicts_scripts})
get_filename_component (dict_name "${dict_script}" NAME_WE)
list (APPEND dicts_targets "dicts-${dict_name}")
add_custom_target (dicts-${dict_name}
COMMAND sh -c "PATH=.:$PATH \"$0\"" "${dict_script}"
DEPENDS tdv-tabfile
COMMENT "Generating sample dictionary ${dict_name}"
VERBATIM)
endforeach ()
add_custom_target (dicts DEPENDS ${dicts_targets})
# The files to be installed
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
if (NOT WIN32)
include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
foreach (page ${project_MAN_PAGES_OUTPUT})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page)
if (WITH_TOOLS)
install (TARGETS ${tools} DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
if (WITH_GUI)
install (FILES ${PROJECT_NAME}.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
install (DIRECTORY ${icon_base}
DESTINATION ${CMAKE_INSTALL_DATADIR})
install (FILES ${PROJECT_NAME}.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install (FILES ${PROJECT_NAME}.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages)
endif ()
foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach ()
elseif (WITH_GUI)
# This rather crude filter has been mostly copied over from logdiag
install (TARGETS ${PROJECT_NAME} DESTINATION .)
install (DIRECTORY
${win32_deps_prefix}/bin/
DESTINATION .
FILES_MATCHING PATTERN "*.dll")
install (DIRECTORY
${win32_deps_prefix}/etc/
DESTINATION etc)
install (DIRECTORY
${win32_deps_prefix}/lib/gdk-pixbuf-2.0
DESTINATION lib
FILES_MATCHING PATTERN "*" PATTERN "*.a" EXCLUDE)
install (DIRECTORY
${win32_deps_prefix}/share/glib-2.0/schemas
DESTINATION share/glib-2.0)
install (DIRECTORY
${win32_deps_prefix}/share/icons/Adwaita
DESTINATION share/icons OPTIONAL)
install (FILES
${win32_deps_prefix}/share/icons/hicolor/index.theme
DESTINATION share/icons/hicolor)
install (DIRECTORY "${icon_base}" DESTINATION share)
install (SCRIPT cmake/Win32Cleanup.cmake)
find_program (GTK_UPDATE_ICON_CACHE_EXECUTABLE gtk-update-icon-cache)
if (NOT GTK_UPDATE_ICON_CACHE_EXECUTABLE)
message (FATAL_ERROR "gtk-update-icon-cache not found")
endif ()
install (CODE "execute_process (COMMAND
sh \"${PROJECT_SOURCE_DIR}/cmake/Win32CleanupAdwaita.sh\"
WORKING_DIRECTORY \${CMAKE_INSTALL_PREFIX})")
install (CODE " # This may speed up program start-up a little bit
execute_process (COMMAND \"${GTK_UPDATE_ICON_CACHE_EXECUTABLE}\"
\"\${CMAKE_INSTALL_PREFIX}/share/icons/Adwaita\")")
endif ()
# Do some unit tests
option (BUILD_TESTING "Build tests" OFF)
set (project_tests stardict)
if (BUILD_TESTING)
enable_testing ()
foreach (name ${project_tests})
find_program (xmlwf_EXECUTABLE xmlwf)
find_program (xmllint_EXECUTABLE xmllint)
foreach (xml ${PROJECT_NAME}.xml ${PROJECT_NAME}.svg)
if (xmlwf_EXECUTABLE)
add_test (test-xmlwf-${xml} ${xmlwf_EXECUTABLE}
"${PROJECT_SOURCE_DIR}/${xml}")
endif ()
if (xmllint_EXECUTABLE)
add_test (test-xmllint-${xml} ${xmllint_EXECUTABLE} --noout
"${PROJECT_SOURCE_DIR}/${xml}")
endif ()
endforeach ()
find_program (dfv_EXECUTABLE desktop-file-validate)
if (dfv_EXECUTABLE)
foreach (df ${PROJECT_NAME}.desktop)
add_test (test-dfv-${df} ${dfv_EXECUTABLE}
"${PROJECT_SOURCE_DIR}/${df}")
endforeach ()
endif ()
foreach (name stardict)
add_executable (test-${name}
src/test-${name}.c ${project_common_sources})
target_link_libraries (test-${name} ${project_common_libraries})
add_test (test-${name} test-${name})
endforeach (name)
endif (BUILD_TESTING)
add_test (NAME test-${name} COMMAND test-${name})
endforeach ()
endif ()
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "StarDict terminal UI")
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Translation dictionary viewer")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR})
set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR})
set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}")
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME} ${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
# XXX: It is still possible to install multiple copies, making commands collide.
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "${PROJECT_NAME}")
set (CPACK_NSIS_INSTALLED_ICON_NAME ${PROJECT_NAME}.exe)
set (CPACK_PACKAGE_EXECUTABLES ${PROJECT_NAME} ${PROJECT_NAME})
set (CPACK_NSIS_EXECUTABLES_DIRECTORY .)
set (CPACK_NSIS_EXTRA_INSTALL_COMMANDS [[
WriteRegStr HKCR '.ifo' '' 'tdv.Dictionary'
WriteRegStr HKCR 'tdv.Dictionary' '' 'StarDict Dictionary'
WriteRegStr HKCR 'tdv.Dictionary\\shell\\open\\command' '' '\"$INSTDIR\\tdv.exe\" \"%1\"'
System::Call 'shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)'
]])
set (CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS [[
DeleteRegKey HKCR 'tdv.Dictionary'
System::Call 'shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)'
]])
include (CPack)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2013 - 2020, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2013 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@@ -1,43 +1,51 @@
StarDict Terminal UI
====================
Translation dictionary viewer
=============================
'sdtui' aims to provide an easy way of viewing translation as well as other
kinds of dictionaries in your terminal. I wasn't successful in finding any free
dictionary software of this kind, GUI or not, and thus decided to write my own.
'tdv' aims to provide an easy way of viewing translation as well as other kinds
of StarDict dictionaries, and is inspired by the dictionary component
of PC Translator. I was unsuccessful in finding any free software of this kind,
and thus decided to write my own.
The program offers both a terminal user interface, and a GTK+ 3 based UI.
The styling of the latter will follow your theme, and may be customized
from 'gtk.css'.
The project is covered by a permissive license, unlike vast majority of other
similar projects, and can serve as a base for implementing other dictionary
software. I wasn't able to reuse _anything_ for StarDict.
software.
Further Development
-------------------
While I've been successfully using sdtui for a long time now, some work has to
be done yet before the software can be considered fit for inclusion in regular
Linux and/or BSD distributions. Help is much appreciated.
An approximate list of things that need to be resolved:
- rewrite the frontend using a proper TUI framework
- possibly make it work better with multiple dictionaries as now it's only
acceptable if you give them short names in the configuration so that they
all fit in the header
- figure out a way to become capable of displaying most dictionaries
Screenshot
----------
image::tdv.png[align="center"]
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR.
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/tdv-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Documentation
-------------
See the link:docs/tdv.1.adoc[man page] for information about usage.
The rest of this README will concern itself with externalities.
Building and Running
--------------------
Build dependencies: CMake, pkg-config, xsltproc, docbook-xsl +
Runtime dependencies: ncursesw, zlib, ICU, termo (included),
glib-2.0, pango, xcb and xcb-xfixes (optional)
Build-only dependencies:
CMake, pkg-config, gettext utilities, asciidoctor or asciidoc +
Optional build-only dependencies:
librsvg (for the GUI), icoutils (for the GUI, when targetting Windows) +
Runtime dependencies:
ncursesw, zlib, ICU, termo (included), glib-2.0 >= 2.38, pango +
Optional runtime dependencies:
xcb, xcb-xfixes (the first two for the TUI), gtk+-3.0 (for the GUI)
$ git clone --recursive https://git.janouch.name/p/sdtui.git
$ mkdir sdtui/build
$ cd sdtui/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DWITH_X11=ON
$ git clone --recursive https://git.janouch.name/p/tdv.git
$ mkdir tdv/build
$ cd tdv/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
-DWITH_X11=ON -DWITH_GUI=ON
$ make
To install the application, you can do either the usual:
@@ -47,70 +55,52 @@ To install the application, you can do either the usual:
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i sdtui-*.deb
# dpkg -i tdv-*.deb
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
Having the program installed, simply run it with a StarDict '.ifo' file as
an argument. It is, however, preferable to
link:docs/tdv.1.adoc#_configuration[configure it] to load your dictionaries
automatically.
Having the program installed, simply run it with a StarDict '.ifo' file as an
argument. It is however highly recommended to configure it, see below.
Windows
~~~~~~~
With the help of Mingw-w64 and WINE, 'tdv' will successfully cross-compile
for Windows. It isn't particularly usable on that system, if only because
selection watching is a very X11/Wayland-specific feature. Beware that build
dependencies take up almost a gigabyte of disk space.
Extensions
----------
As the original StarDict is a bit of a clusterfuck with regard to collation of
dictionary entries, I had to introduce an additional `collation` field into the
'.ifo' file. When sdtui discovers this field while reading the dictionary, it
automatically reorders the index according to that locale (e.g. "cs_CZ").
This operation may take a little while, in the order of seconds.
Configuration
-------------
To get a nicer look in 256color terminals, create _~/.config/sdtui/sdtui.conf_
with the following. Note that it is intended for black-on-white terminals.
....
[Settings]
center-search = true
underline-last = false
hl-common-prefix = true
watch-selection = true
[Colors]
header = reverse
header-active = ul
search = ul
even = 16 231
odd = 16 255
....
The `watch-selection` option makes the application watch the X11 primary
selection for changes and automatically search for selected text.
This feature requires XCB and it will never work on Wayland by its design.
You can also set up some dictionaries to be loaded at startup automatically:
....
[Dictionaries]
name1 = ~/path/to/dict.ifo
name2 = ~/another/dict.ifo
....
$ sh -e cmake/Win64Depends.sh
$ cmake -DCMAKE_TOOLCHAIN_FILE=liberty/cmake/toolchains/MinGW-w64-x64.cmake \
-DCMAKE_BUILD_TYPE=Release -B build
$ cmake --build build -- package
Dictionaries
------------
Unfortunately this application only really works with specific dictionaries.
Word definitions have to be in plain text, separated by newlines.
This application is intended for use with specific dictionaries: each line
should contain one short word definition. Moreover, the only supported content
types are plain text, Pango markup, and XDXF (the visual format works better).
You may use the included transform tool to transform existing dictionaries that
are almost useful as they are, e.g. after stripping XML tags. You might want to
fix up the `sametypesequence` of the resulting '.ifo' file afterwards, and run
dictzip on the resulting '.dict' file.
The `make dicts` command will build some examples from freely available sources:
https://mega.co.nz/#!axtD0QRK!sbtBgizksyfkPqKvKEgr8GQ11rsWhtqyRgUUV0B7pwg[
CZ <--> { EN, DE, PL, RU } dictionaries]
- GNU/FDL Czech-English dictionary
- Czech foreign words (the site's export is broken as of 2022/08, no response)
- Czech WordNet 1.9 PDT (synonyms, hypernyms, hyponyms)
You can use the included 'tdv-transform' tool to convert already existing
StarDict dictionaries that are nearly good as they are. Remember that you can
change the `sametypesequence` of the resulting '.ifo' file to another format,
or run 'dictzip' on '.dict' files to make them compact.
https://mega.co.nz/#!axtD0QRK!sbtBgizksyfkPqKvKEgr8GQ11rsWhtqyRgUUV0B7pwg[CZ <--> EN/DE/PL/RU dictionaries]
Further Development
-------------------
Lacking configuration, standard StarDict locations should be scanned.
We should try harder to display arbitrary dictionaries sensibly.
Contributing and Support
------------------------
Use https://git.janouch.name/p/sdtui to report any bugs, request features,
Use https://git.janouch.name/p/tdv to report any bugs, request features,
or submit pull requests. `git send-email` is tolerated. If you want to discuss
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.

8
cmake/FindLibIntl.cmake Normal file
View File

@@ -0,0 +1,8 @@
# Public Domain
find_library (LibIntl_LIBRARIES intl)
include (FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS (LibIntl DEFAULT_MSG LibIntl_LIBRARIES)
mark_as_advanced (LibIntl_LIBRARIES)

View File

@@ -1,17 +0,0 @@
# Public Domain
find_package (PkgConfig REQUIRED)
pkg_check_modules (Ncursesw QUIET ncursesw)
# OpenBSD doesn't provide a pkg-config file
set (required_vars Ncursesw_LIBRARIES)
if (NOT Ncursesw_FOUND)
find_library (Ncursesw_LIBRARIES NAMES ncursesw)
find_path (Ncursesw_INCLUDE_DIRS ncurses.h)
list (APPEND required_vars Ncursesw_INCLUDE_DIRS)
endif (NOT Ncursesw_FOUND)
include (FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS (Ncursesw DEFAULT_MSG ${required_vars})
mark_as_advanced (Ncursesw_LIBRARIES Ncursesw_INCLUDE_DIRS)

37
cmake/Win32Cleanup.cmake Normal file
View File

@@ -0,0 +1,37 @@
# To be run from cmake_install.cmake, eradicates all unreferenced libraries.
# CMake 3.9.6 has a parsing bug with ENCODING UTF-8.
cmake_minimum_required (VERSION 3.10)
# CPack runs this almost without any CMake variables at all
# (cmStateSnapshot::SetDefaultDefinitions(), CMAKE_INSTALL_PREFIX, [DESTDIR])
set (installdir "${CMAKE_INSTALL_PREFIX}")
if (NOT installdir OR installdir MATCHES "^/usr(/|$)")
return ()
endif ()
# The function is recursive and CMake has tragic scoping behaviour;
# environment variables are truly global there, in the absence of a cache
unset (ENV{seen})
function (expand path)
set (seen $ENV{seen})
if (path IN_LIST seen OR NOT EXISTS "${path}")
return ()
endif ()
set (ENV{seen} "$ENV{seen};${path}")
file (STRINGS "${path}" strings REGEX "[.][Dd][Ll][Ll]$" ENCODING UTF-8)
foreach (string ${strings})
string (REGEX MATCH "[-.+_a-zA-Z0-9]+$" word "${string}")
expand ("${installdir}/${word}")
endforeach ()
endfunction ()
file (GLOB roots LIST_DIRECTORIES false "${installdir}/*.[Ee][Xx][Ee]"
"${installdir}/lib/gdk-pixbuf-2.0/2.10.0/loaders/*.[Dd][Ll][Ll]")
foreach (binary ${roots})
expand ("${binary}")
endforeach ()
file (GLOB libraries LIST_DIRECTORIES false "${installdir}/*.[Dd][Ll][Ll]")
list (REMOVE_ITEM libraries $ENV{seen})
file (REMOVE ${libraries})

View File

@@ -0,0 +1,20 @@
#!/bin/sh -e
# Removes unused icons from the Adwaita theme. It could be even more aggressive,
# since it keeps around lots of sizes and all the GTK+ stock icons.
export LC_ALL=C
find share/icons/Adwaita -type f | awk 'BEGIN {
while (("grep -aho \"[a-z][a-z-]*\" *.dll *.exe" | getline) > 0)
good[$0] = 1
} /[.](png|svg|cur|ani)$/ {
# Cut out the basename without extensions
match($0, /[^\/]+$/)
base = substr($0, RSTART)
sub(/[.].+$/, "", base)
# Try matching while cutting off suffixes
# Disregarding the not-much-used GTK_ICON_LOOKUP_GENERIC_FALLBACK
while (!(keep = good[base]) &&
sub(/-(ltr|rtl|symbolic)$/, "", base)) {}
if (!keep)
print
}' | xargs rm --

74
cmake/Win64Depends.sh Normal file
View File

@@ -0,0 +1,74 @@
#!/bin/sh -e
# Win64Depends.sh: download dependencies from MSYS2 for cross-compilation.
# Dependencies: AWK, sed, sha256sum, cURL, bsdtar, wine64
repository=https://repo.msys2.org/mingw/mingw64/
status() {
echo "$(tput bold)-- $*$(tput sgr0)"
}
dbsync() {
status Fetching repository DB
[ -f db.tsv ] || curl -# "$repository/mingw64.db" | bsdtar -xOf- | awk '
function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
' > db.tsv
}
fetch() {
status Resolving "$@"
mkdir -p packages
awk -F'\t' 'function get(name, i, a) {
if (visited[name]++ || !(name in filenames)) return
print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
} BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
while IFS= read -r name
do
status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
done
}
verify() {
status Verifying checksums
sha256sum --ignore-missing --quiet -c db.sums
}
extract() {
status Extracting packages
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
while IFS= read -r name
do bsdtar -xf "packages/$name" --strip-components 1
done < db.want
}
configure() {
status Configuring packages
glib-compile-schemas share/glib-2.0/schemas
wine64 bin/gdk-pixbuf-query-loaders.exe \
> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
}
# This directory name matches the prefix in .pc files, so we don't need to
# modify them (pkgconf has --prefix-variable, but CMake can't pass that option).
mkdir -p mingw64
cd mingw64
dbsync
fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-icu \
mingw-w64-x86_64-libwinpthread-git # Because we don't do "provides"?
verify
extract
configure
status Success
# XXX: Why is this override needed to run some GLib-based things under wine64?
unset XDG_DATA_DIRS

View File

@@ -2,13 +2,13 @@
#define CONFIG_H
#define PROJECT_NAME "${PROJECT_NAME}"
#define PROJECT_VERSION "${project_VERSION}"
#define PROJECT_URL "${project_URL}"
#define PROJECT_VERSION "${PROJECT_VERSION}"
#define GETTEXT_PACKAGE PROJECT_NAME
#define GETTEXT_DIRNAME "${CMAKE_INSTALL_PREFIX}/share/locale"
#cmakedefine WITH_X11
#cmakedefine WITH_GUI
#cmakedefine HAVE_RESIZETERM
#endif // ! CONFIG_H

85
dicts/czech-wordnet.pl Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env perl
# Czech WordNet 1.9 PDT, CC BY-NC-SA 3.0, newer versions available commercially;
# this one's IDs cannot be linked with any release of the Princeton WordNet
use warnings;
use strict;
# GNU Gzip can unpack a ZIP file, but not the BSD one, and unzip can't use stdin
my $zipcat = qx/(command -v bsdtar)/ ? 'bsdtar -xOf-' : 'zcat';
my $base = 'https://lindat.cz/repository/xmlui';
my $path = 'handle/11858/00-097C-0000-0001-4880-3';
open(my $doc, '-|',
"curl -Lo- '$base/bitstream/$path/Czech_WordNet_1.9_PDT.zip'"
. " | $zipcat | iconv -f latin2 -t UTF-8") or die $!;
# https://nlp.fi.muni.cz/trac/deb2/wiki/WordNetFormat but not quite;
# for terminology see https://wordnet.princeton.edu/documentation/wngloss7wn
my %synsets;
while (<$doc>) {
my $id = m|<ID>(.+?)</ID>| && $1; next unless defined $id;
my $pos = m|<POS>(.+?)</POS>| && $1; next if $pos eq 'e';
$synsets{$id} = {
literals => [map {s| \^\d+||gr} m|<LITERAL>(.+?)<|g],
rels => {
anto => [m^<ILR>(.+?)<TYPE>near_antonym<^g],
hyper => [m^<ILR>(.+?)<TYPE>hypernym<^g],
hypo => [m^<ILR>(.+?)<TYPE>hyponym<^g,
m^<SUBEVENT>(.+?)</SUBEVENT>^g],
super => [m^<ILR>(.+?)<TYPE>holo_part<^g],
sub => [m^<ILR>(.+?)<TYPE>(?:holo_member|mero_part|partonym)<^g],
},
};
}
# Resolve all synset links to hash references, filtering out what can't be found
while (my ($id, $synset) = each %synsets) {
while (my ($name, $links) = each %{$synset->{rels}}) {
@$links = map {$synsets{$_} || ()} @$links;
}
}
# Ensure symmetry in relationships, duplicates will be taken care of later
my %antitags = qw(anto anto hyper hypo hypo hyper super sub sub super);
while (my ($id, $synset) = each %synsets) {
while (my ($name, $links) = each %{$synset->{rels}}) {
push @{$_->{rels}->{$antitags{$name}}}, $synset for @$links;
}
}
# Create an inverse index from literals/keywords to their synsets
my %literals;
while (my ($id, $synset) = each %synsets) {
push @{$literals{$_}}, $synset for @{$synset->{literals}};
}
# Output synsets exploded to individual words, with expanded relationships
close($doc) or die $?;
open(my $tabfile, '|-', 'tdv-tabfile', 'czech-wordnet',
'--book-name=Czech WordNet 1.9 PDT', "--website=$base/$path",
'--date=2011-01-24', '--collation=cs_CZ') or die $!;
sub expand {
my %seen;
return grep {!$seen{$_}++} (map {@{$_->{literals}}} @_);
}
for my $keyword (sort {lc $a cmp lc $b} keys %literals) {
my @lines;
for my $synset (@{$literals{$keyword}}) {
my $rels = $synset->{rels};
push @lines,
(grep {$_ ne $keyword} @{$synset->{literals}}),
(map {"$_ ↑"} expand(@{$rels->{hyper}})),
(map {"$_ ↓"} expand(@{$rels->{hypo}})),
(map {"$_ ⊃"} expand(@{$rels->{super}})),
(map {"$_ ⊂"} expand(@{$rels->{sub}})),
(map {"$_ ≠"} expand(@{$rels->{anto}}));
}
if (@lines) {
print $tabfile "$keyword\t" . join('\n',
map { s/&lt;/</gr =~ s/&gt;/>/gr =~ s/&amp;/&/gr
=~ s/\\/\\\\/gr =~ s/\n/\\n/gr =~ s/\t/\\t/gr} @lines) . "\n";
}
}
close($tabfile) or die $?;

34
dicts/gnu-fdl-de-cz.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/sh -e
# GNU/FDL German-Czech dictionary, see https://gnu.nemeckoceskyslovnik.cz
# Sometimes the domain doesn't resolve, and the contents are close to useless
[ -n "$WANT_BAD_DICTS" ] || exit 0
curl -Lo- 'https://gnu.nemeckoceskyslovnik.cz/index.php?id=6&sablona=export&format=zcu' | \
grep -v ^# | sed 's/\\//g' | perl -CSD -F\\t -le '
sub tabesc { shift =~ s/\\/\\\\/gr =~ s/\n/\\n/gr =~ s/\t/\\t/gr }
sub w {
my ($name, $dict, $collation) = @_;
open(my $f, "|-", "tdv-tabfile", "--pango", "--collation=$collation",
"--website=https://gnu.nemeckoceskyslovnik.cz",
"gnu-fdl-$name") or die $!;
print $f tabesc($keyword) . "\t" . tabesc(join("\n", @$defs))
while ($keyword, $defs) = each %{$dict};
close($f);
}
sub xmlesc { shift =~ s/&/&amp;/gr =~ s/</&lt;/gr =~ s/>/&gt;/gr }
sub entry {
my ($definition, $notes) = map {xmlesc($_)} @_;
$notes ? "$definition <i>$notes</i>" : $definition;
}
next if !$_ .. 0;
my ($de, $cs, $notes, $special, $translator) = @F;
if ($cs) {
$notes =~ s/\w+:\s?//g; # remove word classes
$notes =~ s/(\w+\.)(?!])/($1)/; # quote "pl."
push(@{$decs{$de}}, entry($cs, $notes));
push(@{$csde{$cs}}, entry($de, $notes));
} END {
w("de-cz", \%decs, "de");
w("cz-de", \%csde, "cs");
}'

29
dicts/gnu-fdl-en-cz.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh -e
# GNU/FDL English-Czech dictionary, see https://www.svobodneslovniky.cz/
curl -Lo- https://www.svobodneslovniky.cz/data/en-cs.txt.gz | \
zcat | grep -v ^# | sed 's/\\//g' | perl -CSD -F\\t -le '
sub tabesc { shift =~ s/\\/\\\\/gr =~ s/\n/\\n/gr =~ s/\t/\\t/gr }
sub w {
my ($name, $dict, $collation) = @_;
open(my $f, "|-", "tdv-tabfile", "--pango", "--collation=$collation",
"--website=https://www.svobodneslovniky.cz",
"gnu-fdl-$name") or die $!;
print $f tabesc($keyword) . "\t" . tabesc(join("\n", @$defs))
while ($keyword, $defs) = each %{$dict};
close($f);
}
sub xmlesc { shift =~ s/&/&amp;/gr =~ s/</&lt;/gr =~ s/>/&gt;/gr }
sub entry {
my ($definition, $notes) = map {xmlesc($_)} @_;
$notes ? "$definition <i>$notes</i>" : $definition;
}
my ($en, $cs, $notes, $special, $translator) = @F;
if ($cs) {
$notes =~ s/\w+:\s?//g; # remove word classes
$notes =~ s/(\w+\.)(?!])/($1)/; # quote "pl."
push(@{$encs{$en}}, entry($cs, $notes));
push(@{$csen{$cs}}, entry($en, $notes));
} END {
w("en-cz", \%encs, "en");
w("cz-en", \%csen, "cs");
}'

11
dicts/slovnik-cizich-slov.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh -e
# Slovník cizích slov, see https://slovnik-cizich-slov.abz.cz/web.php/o-slovniku
# XXX: skipping the /optional/ pronunciation field, tabfile can't handle that
curl -Lo- https://slovnik-cizich-slov.abz.cz/export.php | \
iconv -f latin2 -t UTF-8 | perl -CSD -F\\\| -le '
print "$_\t" . $F[2] =~ s/\\/\\\\/gr =~ s/; /\\n/gr for split(", ", $F[0])
' | sort -u | tdv-tabfile slovnik-cizich-slov \
--book-name="Slovník cizích slov" \
--website=https://slovnik-cizich-slov.abz.cz \
--date="$(date +%F)" \
--collation=cs_CZ

View File

@@ -1,64 +0,0 @@
<refentry>
<refentryinfo>
<title>sdtui</title>
<productname>sdtui</productname>
<author>
<firstname>Přemysl</firstname>
<surname>Janouch</surname>
</author>
</refentryinfo>
<refmeta>
<refentrytitle>sdtui</refentrytitle>
<manvolnum>1</manvolnum>
<refmiscinfo class="manual">User Commands</refmiscinfo>
</refmeta>
<refnamediv>
<refname>sdtui</refname>
<refpurpose>StarDict terminal UI</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>sdtui</command>
<arg choice="opt" rep="repeat">
<option><replaceable>OPTION</replaceable></option>
</arg>
<arg choice="opt" rep="repeat">
<replaceable>dictionary.ifo</replaceable>
</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1><title>Description</title>
<para><command>sdtui</command> is a StarDict dictionary viewer custom tailored
for viewing translation dictionaries, using a simple curses-based terminal UI.
</para>
<para>The program expects to find on its command line the path to a dictionary's
.ifo file, which contains further information required for loading the
dictionary.</para>
<para>Some options as well as dictionaries to load on start by default can be
specified in a configuration file. See the README for an example.</para>
</refsect1>
<refsect1><title>Options</title>
<variablelist>
<varlistentry>
<term><option>-h</option>, <option>--help</option></term>
<listitem><para>
show help options
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>-V</option>, <option>--version</option></term>
<listitem><para>
output version information and exit
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
</refentry>

100
docs/tdv.1.adoc Normal file
View File

@@ -0,0 +1,100 @@
tdv(1)
======
:doctype: manpage
:manmanual: tdv Manual
:mansource: tdv {release-version}
Name
----
tdv - Translation dictionary viewer
Synopsis
--------
*tdv* [_OPTION_]... [_DICTIONARY_.ifo]...
Description
-----------
*tdv* is a StarDict dictionary viewer, custom-tailored for translation
dictionaries, with a simple curses-based terminal UI, and a GTK+ graphical UI.
Without any command line arguments, the program expects to find a list of
dictionaries to load on start-up in its configuration file. The _.ifo_ files
contain information required to load dictionaries from their accompanying
database files.
Options
-------
*--gui*::
Launch the GUI even when run from a terminal.
*-h*, *--help*::
Display a help message and exit.
*-V*, *--version*::
Output version information and exit.
Configuration
-------------
You can start your _tdv.conf_ file with the following snippet:
[Settings]
center-search = true # Ensure visibility of preceding entries?
underline-last = false # Underline the last line of entries?
hl-common-prefix = true # Highlight the longest common prefix?
watch-selection = true # Watch X11 selection for changes?
The _watch-selection_ option makes the application watch the X11 PRIMARY
selection for changes and automatically search for any selected text.
This feature requires XCB. Wayland is currently unsupported,
but would require a compositor supporting the wlr-data-control protocol.
Luckily, some compositors, such as Sway, synchronize selections with Xwayland.
To set up automatically loaded dictionaries, use the following scheme:
// AsciiDoc would otherwise like to process tildes as a long subscript.
:tilde: ~
[subs="normal"]
[Dictionaries]
_name 1_ = __{tilde}/path/to/dict.ifo__
_name 2_ = __{tilde}/another/dict.ifo__
The left-hand side keys define their appearance in the tab bar.
Finally, to make the program look nicer in 256color black-on-white terminals,
rather than rely on the universal default, try:
[Colors]
header = reverse
header-active = ul
search = ul
even = 16 231
odd = 16 255
Terminal attributes are accepted in a format similar to that of *git-config*(1),
only named colours aren't supported.
Extensions
----------
Because the StarDict file format is a bit of a clusterfuck with regard to
collation of dictionary entries, this software introduces an additional,
optional "collation" field into the _.ifo_ file. When *tdv* discovers this
field while reading a dictionary, it automatically reorders the index according
to that locale (e.g., "cs_CZ"). This operation may take a little while,
in the order of seconds.
Files
-----
*tdv* follows the XDG Base Directory Specification.
_~/.config/tdv/tdv.conf_::
The configuration file.
Reporting bugs
--------------
Use https://git.janouch.name/p/tdv to report bugs, request features,
or submit pull requests.
See also
--------
*dictzip*(1)

1
liberty Submodule

Submodule liberty added at 0f20cce9c8

112
po/cs.po
View File

@@ -1,131 +1,167 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2013 Přemysl Eric Janouch
# This file is distributed under the same license as the sdtui package.
# This file is distributed under the same license as the tdv package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: sdtui 0.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/pjanouch/sdtui/issues\n"
"POT-Creation-Date: 2016-09-28 16:12+0200\n"
"PO-Revision-Date: 2016-09-28 16:15+0200\n"
"Project-Id-Version: tdv 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/tdv/issues\n"
"POT-Creation-Date: 2023-06-11 17:47+0200\n"
"PO-Revision-Date: 2023-06-11 17:53+0200\n"
"Last-Translator: Přemysl Eric Janouch <p@janouch.name>\n"
"Language-Team: Czech <translation-team-cs@lists.sourceforge.net>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.9\n"
"X-Generator: Poedit 3.3.1\n"
#: ../src/sdtui.c:573
#: ../src/tdv-tui.c:572
msgid "Cannot load configuration"
msgstr "Nemohu načíst konfiguraci"
#: ../src/sdtui.c:1997
#: ../src/tdv.c:77
msgid "Error"
msgstr "Chyba"
#: ../src/sdtui.c:606
#: ../src/tdv-tui.c:592
msgid "Error loading dictionary"
msgstr "Chyba při načítání slovníku"
#: ../src/sdtui.c:612
msgid ""
"No dictionaries found either in the configuration or on the command line"
#: ../src/tdv.c:60
msgid "Launch the GUI even when run from a terminal"
msgstr "Spustit GUI i při běhu z terminálu"
#: ../src/tdv-gui.c:467 ../src/tdv-tui.c:597
msgid "No dictionaries found either in the configuration or on the command line"
msgstr "V konfiguraci ani na příkazové řádce nebyly nalezeny žádné slovníky"
#: ../src/sdtui.c:1976
#: ../src/tdv-gui.c:367
msgid "Open dictionary"
msgstr "Otevřít slovník"
#: ../src/tdv.c:55
msgid "Output version information and exit"
msgstr "Vypíše informace o verzi a ukončí se"
#: ../src/sdtui.c:548
#: ../src/tdv-tui.c:555
msgid "Search"
msgstr "Hledat"
#: ../src/sdtui.c:966
#: ../src/tdv-tui.c:1068
msgid "Terminal UI for StarDict dictionaries"
msgstr "Terminálové UI pro stardictové slovníky"
#: ../src/sdtui.c:969
#: ../src/tdv-tui.c:1071
msgid "Type to search"
msgstr "Zadejte vyhledávaný výraz"
#: ../src/sdtui.c:1981
#: ../src/tdv.c:39
msgid "Warning"
msgstr "Varování"
#: ../src/sdtui.c:1989
msgid "[dictionary.ifo...] - StarDict terminal UI"
msgstr "[slovník.ifo...] - terminálové UI pro StarDict"
#: ../src/tdv-tui.c:2071
#, c-format
msgid "X11 connection failed (error code %d)"
msgstr "Spojení s X11 selhalo (chybový kód %d)"
#: ../src/stardict.c:835
#: ../src/tdv-tui.c:2217
#, c-format
msgid "X11 request error (%d, major %d, minor %d)"
msgstr "Chyba X11 požadavku (%d, major %d, minor %d)"
#: ../src/tdv.c:67
msgid "[dictionary.ifo...] - Translation dictionary viewer"
msgstr "[slovník.ifo...] - Prohlížeč překladových slovníků"
#: ../src/tdv-gui.c:369
msgid "_Cancel"
msgstr "_Storno"
#: ../src/tdv-gui.c:509
msgid "_Follow selection"
msgstr "_Sledovat výběr"
#: ../src/tdv-gui.c:370
msgid "_Open"
msgstr "_Otevřít"
#: ../src/tdv-gui.c:504
msgid "_Open..."
msgstr "_Otevřít..."
#: ../src/stardict.c:850
msgid "cannot find .dict file"
msgstr "nemohu najít .dict soubor"
#: ../src/stardict.c:812
#: ../src/stardict.c:827
msgid "cannot find .idx file"
msgstr "nemohu najít .idx soubor"
#: ../src/sdtui.c:283
#: ../src/tdv-tui.c:258
msgid "error in entry"
msgstr "chyba v záznamu"
#: ../src/sdtui.c:1981
#: ../src/tdv.c:39
msgid "failed to set the locale"
msgstr "selhalo nastavení locale"
#: ../src/stardict.c:308
#: ../src/stardict.c:328
msgid "index file size not specified"
msgstr "nebyla určena velikost rejstříku"
#: ../src/stardict.c:1130 ../src/stardict.c:1155
#: ../src/stardict.c:1157 ../src/stardict.c:1182
msgid "invalid data entry"
msgstr "neplatná datová položka"
#: ../src/stardict.c:259
#: ../src/stardict.c:281
msgid "invalid encoding, must be valid UTF-8"
msgstr "neplatné kódování, musí být validní UTF-8"
#: ../src/stardict.c:91
#: ../src/stardict.c:89
msgid "invalid header format"
msgstr "neplatný formát hlavičky"
#: ../src/stardict.c:317
#: ../src/stardict.c:337
msgid "invalid index offset bits"
msgstr "neplatný počet bitů pro offset v rejstříku"
#: ../src/stardict.c:276
#: ../src/stardict.c:298
msgid "invalid integer"
msgstr "neplatné číslo"
#: ../src/stardict.c:238
#: ../src/stardict.c:260
msgid "invalid version"
msgstr "neplatná verze"
#: ../src/stardict.c:296
#: ../src/stardict.c:316
msgid "no book name specified"
msgstr "nebyl určen název knihy"
#: ../src/sdtui.c:302
#: ../src/stardict-view.c:96 ../src/tdv-tui.c:340
msgid "no usable field found"
msgstr "nenalezeno žádné použitelné pole"
#: ../src/stardict.c:286
#: ../src/stardict.c:308
msgid "option format error"
msgstr "chyba v zápisu volby"
#: ../src/sdtui.c:1997
#: ../src/tdv.c:77
msgid "option parsing failed"
msgstr "zpracování přepínačů selhalo"
#: ../src/stardict.c:252
#: ../src/stardict.c:274
msgid "unknown key, ignoring"
msgstr "neznámý klíč, ignoruji"
#: ../src/stardict.c:227
#: ../src/stardict.c:249
msgid "version not specified"
msgstr "nebyla určena verze"
#: ../src/stardict.c:302
#: ../src/stardict.c:322
msgid "word count not specified"
msgstr "nebyl určen počet slov"
#~ msgid "FILE..."
#~ msgstr "SOUBOR..."

View File

@@ -1,47 +1,21 @@
#!/bin/sh
#!/bin/sh -e
# This shell script generates the translation template.
#
# The reason for this not being inside CMakeLists.txt
# is that the translator should not need to run the whole
# configuration process just to get this single stupid file.
# The reason for this not being inside CMakeLists.txt is that the translator
# should not need to run the whole configuration process just to get this file.
dir=$(dirname $0)
# Get the directory this script resides in so that the user
# doesn't have to run the script from there
DIR=$(dirname $0)
export LC_ALL=C
# Collect source files
SOURCES=$(echo $DIR/../src/*.c)
# Get the package name from CMakeLists.txt
PACKAGE=$(sed -n '/^[ \t]*project[ \t]*([ \t]*\([^ \t)]\{1,\}\).*).*/{s//\1/p;q}' \
$DIR/../CMakeLists.txt)
# Get the package version from CMakeLists.txt
EXP_BEG='/^[ \t]*set[ \t]*([ \t]*project_VERSION_'
EXP_END='[ \t]\{1,\}"\{0,1\}\([^)"]\{1,\}\)"\{0,1\}).*/{s//\1/p;q}'
MAJOR=$(sed -n "${EXP_BEG}MAJOR${EXP_END}" $DIR/../CMakeLists.txt)
MINOR=$(sed -n "${EXP_BEG}MINOR${EXP_END}" $DIR/../CMakeLists.txt)
PATCH=$(sed -n "${EXP_BEG}PATCH${EXP_END}" $DIR/../CMakeLists.txt)
if [ "$MAJOR" != "" ]; then
VERSION=$MAJOR
if [ "$MINOR" != "" ]; then
VERSION=$VERSION.$MINOR
if [ "$PATCH" != "" ]; then
VERSION=$VERSION.$PATCH
fi
fi
fi
if [ -z "$PACKAGE" -o -z "$VERSION" ]; then
re='^[ \t]*project *( *\([^ \t)]\{1,\}\) \{1,\}VERSION \{1,\}\([^ \t)]\{1,\}\).*'
package=$(sed -n "s/$re/\\1/p" "$dir/../CMakeLists.txt")
version=$(sed -n "s/$re/\\2/p" "$dir/../CMakeLists.txt")
if [ -z "$package" -o -z "$version" ]; then
echo "Failed to get information from CMakeLists.txt"
exit 1
fi
# Finally make the template
xgettext -LC -k_ -kN_ $SOURCES -o "$DIR/$PACKAGE.pot" \
--package-name="$PACKAGE" --package-version="$VERSION" \
xgettext -LC -k_ -kN_ "$dir"/../src/*.c -o "$dir/$package.pot" \
--package-name="$package" --package-version="$version" \
--copyright-holder="Přemysl Eric Janouch" \
--msgid-bugs-address="https://github.com/pjanouch/$PACKAGE/issues"
--msgid-bugs-address="https://git.janouch.name/p/$package/issues"

View File

@@ -1,14 +1,14 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Přemysl Eric Janouch
# This file is distributed under the same license as the sdtui package.
# This file is distributed under the same license as the tdv package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: sdtui 0.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/pjanouch/sdtui/issues\n"
"POT-Creation-Date: 2016-09-28 16:12+0200\n"
"Project-Id-Version: tdv 0.1.0\n"
"Report-Msgid-Bugs-To: https://git.janouch.name/p/tdv/issues\n"
"POT-Creation-Date: 2023-06-11 17:47+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,115 +17,149 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../src/sdtui.c:283
msgid "error in entry"
msgstr ""
#: ../src/sdtui.c:302
#: ../src/stardict-view.c:96 ../src/tdv-tui.c:340
msgid "no usable field found"
msgstr ""
#: ../src/sdtui.c:548
msgid "Search"
#: ../src/stardict.c:89
msgid "invalid header format"
msgstr ""
#: ../src/sdtui.c:573
msgid "Cannot load configuration"
#: ../src/stardict.c:249
msgid "version not specified"
msgstr ""
#: ../src/sdtui.c:606
msgid "Error loading dictionary"
#: ../src/stardict.c:260
msgid "invalid version"
msgstr ""
#: ../src/sdtui.c:612
#: ../src/stardict.c:274
msgid "unknown key, ignoring"
msgstr ""
#: ../src/stardict.c:281
msgid "invalid encoding, must be valid UTF-8"
msgstr ""
#: ../src/stardict.c:298
msgid "invalid integer"
msgstr ""
#: ../src/stardict.c:308
msgid "option format error"
msgstr ""
#: ../src/stardict.c:316
msgid "no book name specified"
msgstr ""
#: ../src/stardict.c:322
msgid "word count not specified"
msgstr ""
#: ../src/stardict.c:328
msgid "index file size not specified"
msgstr ""
#: ../src/stardict.c:337
msgid "invalid index offset bits"
msgstr ""
#: ../src/stardict.c:827
msgid "cannot find .idx file"
msgstr ""
#: ../src/stardict.c:850
msgid "cannot find .dict file"
msgstr ""
#: ../src/stardict.c:1157 ../src/stardict.c:1182
msgid "invalid data entry"
msgstr ""
#: ../src/tdv-gui.c:367
msgid "Open dictionary"
msgstr ""
#: ../src/tdv-gui.c:369
msgid "_Cancel"
msgstr ""
#: ../src/tdv-gui.c:370
msgid "_Open"
msgstr ""
#: ../src/tdv-gui.c:467 ../src/tdv-tui.c:597
msgid ""
"No dictionaries found either in the configuration or on the command line"
msgstr ""
#: ../src/sdtui.c:966
#: ../src/tdv-gui.c:504
msgid "_Open..."
msgstr ""
#: ../src/tdv-gui.c:509
msgid "_Follow selection"
msgstr ""
#: ../src/tdv-tui.c:258
msgid "error in entry"
msgstr ""
#: ../src/tdv-tui.c:555
msgid "Search"
msgstr ""
#: ../src/tdv-tui.c:572
msgid "Cannot load configuration"
msgstr ""
#: ../src/tdv-tui.c:592
msgid "Error loading dictionary"
msgstr ""
#: ../src/tdv-tui.c:1068
msgid "Terminal UI for StarDict dictionaries"
msgstr ""
#: ../src/sdtui.c:969
#: ../src/tdv-tui.c:1071
msgid "Type to search"
msgstr ""
#: ../src/sdtui.c:1976
msgid "Output version information and exit"
#: ../src/tdv-tui.c:2071
#, c-format
msgid "X11 connection failed (error code %d)"
msgstr ""
#: ../src/sdtui.c:1981
#: ../src/tdv-tui.c:2217
#, c-format
msgid "X11 request error (%d, major %d, minor %d)"
msgstr ""
#: ../src/tdv.c:39
msgid "Warning"
msgstr ""
#: ../src/sdtui.c:1981
#: ../src/tdv.c:39
msgid "failed to set the locale"
msgstr ""
#: ../src/sdtui.c:1989
msgid "[dictionary.ifo...] - StarDict terminal UI"
#: ../src/tdv.c:55
msgid "Output version information and exit"
msgstr ""
#: ../src/sdtui.c:1997
#: ../src/tdv.c:60
msgid "Launch the GUI even when run from a terminal"
msgstr ""
#: ../src/tdv.c:67
msgid "[dictionary.ifo...] - Translation dictionary viewer"
msgstr ""
#: ../src/tdv.c:77
msgid "Error"
msgstr ""
#: ../src/sdtui.c:1997
#: ../src/tdv.c:77
msgid "option parsing failed"
msgstr ""
#: ../src/stardict.c:91
msgid "invalid header format"
msgstr ""
#: ../src/stardict.c:227
msgid "version not specified"
msgstr ""
#: ../src/stardict.c:238
msgid "invalid version"
msgstr ""
#: ../src/stardict.c:252
msgid "unknown key, ignoring"
msgstr ""
#: ../src/stardict.c:259
msgid "invalid encoding, must be valid UTF-8"
msgstr ""
#: ../src/stardict.c:276
msgid "invalid integer"
msgstr ""
#: ../src/stardict.c:286
msgid "option format error"
msgstr ""
#: ../src/stardict.c:296
msgid "no book name specified"
msgstr ""
#: ../src/stardict.c:302
msgid "word count not specified"
msgstr ""
#: ../src/stardict.c:308
msgid "index file size not specified"
msgstr ""
#: ../src/stardict.c:317
msgid "invalid index offset bits"
msgstr ""
#: ../src/stardict.c:812
msgid "cannot find .idx file"
msgstr ""
#: ../src/stardict.c:835
msgid "cannot find .dict file"
msgstr ""
#: ../src/stardict.c:1130 ../src/stardict.c:1155
msgid "invalid data entry"
msgstr ""

View File

@@ -1,174 +0,0 @@
/*
* A tool to query multiple dictionaries for the specified word
*
* Intended for use in IRC bots and similar silly things---words go in, one
* on a line, and entries come out, one dictionary at a time, finalised with
* an empty line. Newlines are escaped with `\n', backslashes with `\\'.
*
* So far only the `m' field is supported. Feel free to extend the program
* according to your needs, it's not very complicated.
*
* Copyright (c) 2013, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include <gio/gio.h>
#include "stardict.h"
#include "stardict-private.h"
#include "generator.h"
static guint
count_equal_chars (const gchar *a, const gchar *b)
{
guint count = 0;
while (*a && *b)
if (*a++ == *b++)
count++;
return count;
}
static void
do_dictionary (StardictDict *dict, const gchar *word)
{
gboolean found;
StardictIterator *iter = stardict_dict_search (dict, word, &found);
if (!found)
goto out;
// Default Stardict ordering is ASCII case-insensitive.
// Try to find a better matching entry based on letter case:
gint64 best_offset = stardict_iterator_get_offset (iter);
guint best_score = count_equal_chars
(stardict_iterator_get_word (iter), word);
while (TRUE)
{
stardict_iterator_next (iter);
if (!stardict_iterator_is_valid (iter))
break;
const gchar *iter_word = stardict_iterator_get_word (iter);
if (g_ascii_strcasecmp (iter_word, word))
break;
guint score = count_equal_chars (iter_word, word);
if (score > best_score)
{
best_offset = stardict_iterator_get_offset (iter);
best_score = score;
}
}
stardict_iterator_set_offset (iter, best_offset, FALSE);
StardictEntry *entry = stardict_iterator_get_entry (iter);
StardictInfo *info = stardict_dict_get_info (dict);
const GList *list = stardict_entry_get_fields (entry);
for (; list; list = list->next)
{
StardictEntryField *field = list->data;
if (field->type == STARDICT_FIELD_MEANING)
{
const gchar *desc = field->data;
printf ("%s:", info->book_name);
for (; *desc; desc++)
{
if (*desc == '\\')
printf ("\\\\");
else if (*desc == '\n')
printf ("\\n");
else
putchar (*desc);
}
putchar ('\n');
}
}
g_object_unref (entry);
out:
g_object_unref (iter);
}
int
main (int argc, char *argv[])
{
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
if (glib_check_version (2, 36, 0))
g_type_init ();
G_GNUC_END_IGNORE_DEPRECATIONS
GError *error = NULL;
GOptionContext *ctx = g_option_context_new
("DICTIONARY.ifo... - query multiple dictionaries");
if (!g_option_context_parse (ctx, &argc, &argv, &error))
{
g_printerr ("Error: option parsing failed: %s\n", error->message);
exit (EXIT_FAILURE);
}
g_option_context_free (ctx);
if (argc < 2)
{
g_printerr ("Error: no dictionaries given\n");
exit (EXIT_FAILURE);
}
guint n_dicts = argc - 1;
StardictDict **dicts = g_alloca (sizeof *dicts * n_dicts);
guint i;
for (i = 1; i <= n_dicts; i++)
{
dicts[i - 1] = stardict_dict_new (argv[i], &error);
if (error)
{
g_printerr ("Error: opening dictionary `%s' failed: %s\n",
argv[i], error->message);
exit (EXIT_FAILURE);
}
}
while (TRUE)
{
GString *s = g_string_new (NULL);
gint c;
while ((c = getchar ()) != EOF && c != '\n')
if (c != '\r')
g_string_append_c (s, c);
if (s->len)
for (i = 0; i < n_dicts; i++)
do_dictionary (dicts[i], s->str);
printf ("\n");
fflush (NULL);
g_string_free (s, TRUE);
if (c == EOF)
break;
}
for (i = 0; i < n_dicts; i++)
g_object_unref (dicts[i]);
return 0;
}

View File

@@ -1,418 +0,0 @@
/*
* StarDict GTK+ UI
*
* Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <locale.h>
#include "config.h"
#include "stardict.h"
#include "utils.h"
typedef struct dictionary Dictionary;
struct dictionary
{
const gchar *filename; ///< Filename
StardictDict *dict; ///< Stardict dictionary data
gchar *name; ///< Name to show
guint position; ///< Current position
};
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *entry; ///< Search entry widget
GtkWidget *grid; ///< Entries container
gint dictionary; ///< Index of the current dictionary
Dictionary *dictionaries; ///< All open dictionaries
gsize dictionaries_len; ///< Total number of dictionaries
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
dictionary_load (Dictionary *self, gchar *filename, GError **e)
{
self->filename = filename;
if (!(self->dict = stardict_dict_new (self->filename, e)))
return FALSE;
if (!self->name)
{
self->name = g_strdup (stardict_info_get_book_name
(stardict_dict_get_info (self->dict)));
}
return TRUE;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
init (gchar **filenames, GError **e)
{
while (filenames[g.dictionaries_len])
g.dictionaries_len++;
g.dictionaries = g_malloc0_n (sizeof *g.dictionaries, g.dictionaries_len);
for (gsize i = 0; i < g.dictionaries_len; i++)
{
Dictionary *dict = &g.dictionaries[i];
if (!dictionary_load (dict, filenames[i], e))
return FALSE;
}
return TRUE;
}
static void
add_row (StardictIterator *iterator, gint row, gint *height_acc)
{
Dictionary *dict = &g.dictionaries[g.dictionary];
StardictEntry *entry = stardict_iterator_get_entry (iterator);
g_return_if_fail (entry != NULL);
StardictEntryField *field = entry->fields->data;
g_return_if_fail (g_ascii_islower (field->type));
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
g_return_if_fail (input_utf8 != NULL);
const gchar *word_str = stardict_iterator_get_word (iterator);
gsize common_prefix = stardict_longest_common_collation_prefix
(dict->dict, word_str, input_utf8);
gchar *pre = g_markup_escape_text (word_str, common_prefix),
*post = g_markup_escape_text (word_str + common_prefix, -1),
*marked_up = g_strdup_printf ("<u>%s</u>%s", pre, post);
GtkWidget *word = gtk_label_new (marked_up);
gtk_label_set_use_markup (GTK_LABEL (word), TRUE);
gtk_label_set_ellipsize (GTK_LABEL (word), PANGO_ELLIPSIZE_END);
gtk_label_set_selectable (GTK_LABEL (word), TRUE);
gtk_label_set_xalign (GTK_LABEL (word), 0);
gtk_label_set_yalign (GTK_LABEL (word), 0);
// FIXME: they can't be deselected by just clicking outside of them
gtk_widget_set_can_focus (word, FALSE);
g_free (pre);
g_free (post);
g_free (marked_up);
GtkWidget *desc = gtk_label_new (field->data);
gtk_label_set_ellipsize (GTK_LABEL (desc), PANGO_ELLIPSIZE_END);
gtk_label_set_selectable (GTK_LABEL (desc), TRUE);
gtk_label_set_xalign (GTK_LABEL (desc), 0);
gtk_widget_set_can_focus (desc, FALSE);
g_object_unref (entry);
if (iterator->offset % 2 == 0)
{
GtkStyleContext *ctx;
ctx = gtk_widget_get_style_context (word);
gtk_style_context_add_class (ctx, "odd");
ctx = gtk_widget_get_style_context (desc);
gtk_style_context_add_class (ctx, "odd");
}
gtk_grid_attach (GTK_GRID (g.grid), word, 0, row, 1, 1);
gtk_grid_attach (GTK_GRID (g.grid), desc, 1, row, 1, 1);
gtk_widget_show (word);
gtk_widget_show (desc);
gint minimum_word = 0, minimum_desc = 0;
gtk_widget_get_preferred_height (word, &minimum_word, NULL);
gtk_widget_get_preferred_height (desc, &minimum_desc, NULL);
*height_acc += MAX (minimum_word, minimum_desc);
}
static void
reload (GtkWidget *grid)
{
Dictionary *dict = &g.dictionaries[g.dictionary];
GList *children = gtk_container_get_children (GTK_CONTAINER (grid));
for (GList *iter = children; iter != NULL; iter = g_list_next (iter))
gtk_widget_destroy (GTK_WIDGET (iter->data));
g_list_free (children);
gint window_height = 0;
gtk_window_get_size (GTK_WINDOW (g.window), NULL, &window_height);
if (window_height <= 0)
return;
StardictIterator *iterator =
stardict_iterator_new (dict->dict, dict->position);
gint row = 0, height_acc = 0;
while (stardict_iterator_is_valid (iterator))
{
add_row (iterator, row++, &height_acc);
if (height_acc >= window_height)
break;
stardict_iterator_next (iterator);
}
gtk_widget_show_all (grid);
g_object_unref (iterator);
}
static void
search (Dictionary *dict)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
StardictIterator *iterator =
stardict_dict_search (dict->dict, input_utf8, NULL);
dict->position = stardict_iterator_get_offset (iterator);
g_object_unref (iterator);
}
static void
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
search (&g.dictionaries[g.dictionary]);
reload (g.grid);
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gtk_entry_set_text (GTK_ENTRY (g.entry), text);
g_signal_emit_by_name (g.entry,
"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
}
static void
on_selection (GtkClipboard *clipboard, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (g.watch_selection
&& event->owner_change.owner != NULL)
gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
}
static void
on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
{
g.watch_selection = gtk_check_menu_item_get_active (item);
}
static void
on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
guint page_num, G_GNUC_UNUSED gpointer data)
{
g.dictionary = page_num;
search (&g.dictionaries[g.dictionary]);
reload (g.grid);
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (event->key.state == GDK_CONTROL_MASK)
{
if (event->key.keyval == GDK_KEY_Page_Up)
{
gtk_notebook_prev_page (GTK_NOTEBOOK (g.notebook));
return TRUE;
}
if (event->key.keyval == GDK_KEY_Page_Down)
{
gtk_notebook_next_page (GTK_NOTEBOOK (g.notebook));
return TRUE;
}
}
if (event->key.state == GDK_MOD1_MASK)
{
if (event->key.keyval >= GDK_KEY_0
&& event->key.keyval <= GDK_KEY_9)
{
gint n = event->key.keyval - GDK_KEY_0;
gtk_notebook_set_current_page
(GTK_NOTEBOOK (g.notebook), n ? (n - 1) : 10);
return TRUE;
}
}
return FALSE;
}
static void
on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
gtk_main_quit ();
}
static void
die_with_dialog (const gchar *message)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
exit (EXIT_FAILURE);
}
int
main (int argc, char *argv[])
{
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
gchar **filenames = NULL;
GOptionEntry option_entries[] =
{
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
NULL, N_("FILE...")},
{},
};
GError *error = NULL;
gtk_init_with_args (&argc, &argv, N_("- StarDict GTK+ UI"),
option_entries, GETTEXT_PACKAGE, &error);
if (error)
{
g_warning ("%s", error->message);
g_error_free (error);
return 1;
}
if (!filenames)
{
// TODO: eventually just load all dictionaries from configuration
die_with_dialog ("No arguments have been passed.");
}
if (!init (filenames, &error))
die_with_dialog (error->message);
// Some Adwaita stupidity and our own additions
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
"grid { border-top: 1px solid rgba(0, 0, 0, 0.2); background: white; }"
"grid label { padding: 0 5px; "
"/*border-bottom: 1px solid rgba(0, 0, 0, 0.2);*/ }"
"grid label.odd { background: rgba(0, 0, 0, 0.05); }";
GdkScreen *screen = gdk_screen_get_default ();
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g.grid = gtk_grid_new ();
gtk_grid_set_column_homogeneous (GTK_GRID (g.grid), TRUE);
// FIXME: we'd rather like to trim the contents, not make it scrollable.
// This just limits the allocation.
// TODO: probably create a whole new custom widget, everything is text
// anyway and mostly handled by Pango, including pango_layout_xy_to_index()
// - I don't know where to get selection colour but inversion works, too
GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL);
gtk_widget_set_can_focus (scrolled_window, FALSE);
gtk_container_add (GTK_CONTAINER (scrolled_window), g.grid);
g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL);
gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
g.watch_selection = TRUE;
GtkWidget *item =
gtk_check_menu_item_new_with_label (_("Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item), g.watch_selection);
g_signal_connect (item, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
gtk_widget_show_all (menu);
GtkWidget *hamburger = gtk_menu_button_new ();
gtk_menu_button_set_direction (GTK_MENU_BUTTON (hamburger), GTK_ARROW_NONE);
gtk_menu_button_set_popup (GTK_MENU_BUTTON (hamburger), menu);
gtk_button_set_relief (GTK_BUTTON (hamburger), GTK_RELIEF_NONE);
gtk_widget_show (hamburger);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), hamburger, GTK_PACK_END);
// FIXME: when the clear icon shows, the widget changes in height
g.entry = gtk_search_entry_new ();
// TODO: attach to the "key-press-event" signal and implement ^W at least,
// though ^U is working already! Note that bindings can be done in CSS
// as well, if we have any extra specially for the editor
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.grid);
gtk_entry_set_has_frame (GTK_ENTRY (g.entry), FALSE);
// TODO: supposedly attach to "key-press-event" here and react to
// PageUp/PageDown and up/down arrow keys... either here or in the Entry
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_signal_connect (g.window, "destroy",
G_CALLBACK (on_destroy), NULL);
g_signal_connect (g.window, "key-press-event",
G_CALLBACK (on_key_press), NULL);
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
gtk_container_add (GTK_CONTAINER (g.window), superbox);
gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
gtk_container_add (GTK_CONTAINER (superbox), g.entry);
gtk_box_pack_end (GTK_BOX (superbox), scrolled_window, TRUE, TRUE, 0);
for (gsize i = 0; i < g.dictionaries_len; i++)
{
Dictionary *dict = &g.dictionaries[i];
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_append_page (GTK_NOTEBOOK (g.notebook), dummy, label);
}
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
// Make sure to fill up the window with entries once we're resized
// XXX: this is rather inefficient as we rebuild everything each time
g_signal_connect (g.window, "configure-event",
G_CALLBACK (on_changed), NULL);
g_signal_connect (g.window, "map-event",
G_CALLBACK (on_changed), NULL);
gtk_widget_grab_focus (g.entry);
gtk_widget_show_all (g.window);
gtk_main ();
g_strfreev (filenames);
return 0;
}

View File

@@ -79,4 +79,6 @@ extern const struct stardict_ifo_key _stardict_ifo_keys[];
/// Denotes the length of _stardict_ifo_keys.
extern gsize _stardict_ifo_keys_length;
void stardict_info_copy (StardictInfo *dest, const StardictInfo *src);
#endif // ! STARDICTPRIVATE_H

1221
src/stardict-view.c Normal file

File diff suppressed because it is too large Load Diff

36
src/stardict-view.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* StarDict GTK+ UI - dictionary view component
*
* Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#ifndef STARDICT_VIEW_H
#define STARDICT_VIEW_H
#include <gtk/gtk.h>
#include "stardict.h"
#define STARDICT_TYPE_VIEW (stardict_view_get_type ())
G_DECLARE_FINAL_TYPE (StardictView, stardict_view, STARDICT, VIEW, GtkWidget)
GtkWidget *stardict_view_new (void);
void stardict_view_set_position (StardictView *view,
StardictDict *dict, guint position);
void stardict_view_set_matched (StardictView *view, const gchar *matched);
void stardict_view_scroll (StardictView *view,
GtkScrollStep step, gdouble amount);
#endif // ! STARDICT_VIEW_H

View File

@@ -209,6 +209,30 @@ const struct stardict_ifo_key _stardict_ifo_keys[] =
gsize _stardict_ifo_keys_length = G_N_ELEMENTS (_stardict_ifo_keys);
/// Copy the contents of one StardictInfo object into another. Ignores path.
void
stardict_info_copy (StardictInfo *dest, const StardictInfo *src)
{
dest->version = src->version;
guint i;
for (i = 0; i < _stardict_ifo_keys_length; i++)
{
const struct stardict_ifo_key *key = &_stardict_ifo_keys[i];
if (key->type == IFO_STRING)
{
gchar **p = &G_STRUCT_MEMBER (gchar *, dest, key->offset);
gchar *q = G_STRUCT_MEMBER (gchar *, src, key->offset);
g_free (*p);
*p = q ? g_strdup (q) : NULL;
}
else
G_STRUCT_MEMBER (gulong, dest, key->offset) =
G_STRUCT_MEMBER (gulong, src, key->offset);
}
}
static gboolean
load_ifo (StardictInfo *sti, const gchar *path, GError **error)
{
@@ -285,26 +309,24 @@ load_ifo (StardictInfo *sti, const gchar *path, GError **error)
goto error;
}
ret_val = TRUE;
// FIXME check for zeros, don't assume that 0 means for "not set"
// FIXME check for zeros, don't assume that 0 means "not set"
if (!sti->book_name || !*sti->book_name)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("no book name specified"));
ret_val = FALSE;
goto error;
}
if (!sti->word_count)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("word count not specified"));
ret_val = FALSE;
goto error;
}
if (!sti->idx_filesize)
{
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s", path, _("index file size not specified"));
ret_val = FALSE;
goto error;
}
if (!sti->idx_offset_bits)
@@ -314,9 +336,11 @@ load_ifo (StardictInfo *sti, const gchar *path, GError **error)
g_set_error (error, STARDICT_ERROR, STARDICT_ERROR_INVALID_DATA,
"%s: %s: %lu", path, _("invalid index offset bits"),
sti->idx_offset_bits);
ret_val = FALSE;
goto error;
}
ret_val = TRUE;
error:
if (!ret_val)
{
@@ -333,6 +357,20 @@ error:
return ret_val;
}
/// Read an .ifo file.
/// @return StardictInfo *. Deallocate with stardict_info_free();
StardictInfo *
stardict_info_new (const gchar *filename, GError **error)
{
StardictInfo *ifo = g_new (StardictInfo, 1);
if (!load_ifo (ifo, filename, error))
{
g_free (ifo);
return NULL;
}
return ifo;
}
/// List all dictionary files located in a path.
/// @return GList<StardictInfo *>. Deallocate the list with:
/// @code
@@ -353,12 +391,10 @@ stardict_list_dictionaries (const gchar *path)
continue;
gchar *filename = g_build_filename (path, name, NULL);
StardictInfo *ifo = g_new (StardictInfo, 1);
if (load_ifo (ifo, filename, NULL))
dicts = g_list_append (dicts, ifo);
else
g_free (ifo);
StardictInfo *ifo = stardict_info_new (filename, NULL);
g_free (filename);
if (ifo)
dicts = g_list_append (dicts, ifo);
}
g_dir_close (dir);
g_pattern_spec_free (ps);
@@ -867,11 +903,12 @@ stardict_dict_cmp_synonym (StardictDict *sd, const gchar *word, gint i)
g_array_index (synonyms, StardictSynonymEntry, i).word);
}
/// Return words for which the argument is a synonym of or NULL
/// Return words of which the argument is a synonym or NULL
/// if there are no such words.
gchar **
stardict_dict_get_synonyms (StardictDict *sd, const gchar *word)
{
GArray *collated = sd->priv->collated_synonyms;
GArray *synonyms = sd->priv->synonyms;
GArray *index = sd->priv->index;
@@ -879,26 +916,32 @@ stardict_dict_get_synonyms (StardictDict *sd, const gchar *word)
stardict_dict_cmp_synonym (sd, word, imid))
// Back off to the first matching entry
while (imid > 0 && !stardict_dict_cmp_synonym (sd, word, --imid))
;
while (imid > 0 && !stardict_dict_cmp_synonym (sd, word, imid - 1))
imid--;
GPtrArray *array = g_ptr_array_new ();
// And add all matching entries from that position on to the array
do
{
guint32 i = g_array_index
(synonyms, StardictSynonymEntry, ++imid).original_word;
guint32 i = sd->priv->collator
? g_array_index (synonyms, StardictSynonymEntry,
g_array_index (collated, guint32, imid)).original_word
: g_array_index (synonyms, StardictSynonymEntry,
imid).original_word;
// When we use a collator this will point to the original entry,
// otherwise it points to itself and this changes nothing
i = g_array_index
(sd->priv->index, StardictIndexEntry, i).reverse_index;
g_ptr_array_add (array, g_strdup (g_array_index
(index, StardictIndexEntry, i).name));
}
while ((guint) imid < synonyms->len - 1 && !stardict_strcmp (word,
g_array_index (synonyms, StardictSynonymEntry, imid + 1).word));
i = g_array_index (sd->priv->index, StardictIndexEntry,
i).reverse_index;
g_ptr_array_add (array,
g_strdup (g_array_index (index, StardictIndexEntry, i).name));
}
while ((guint) ++imid < synonyms->len
&& !stardict_dict_cmp_synonym (sd, word, imid));
g_ptr_array_add (array, NULL);
return (gchar **) g_ptr_array_free (array, FALSE);
BINARY_SEARCH_END
@@ -983,6 +1026,10 @@ stardict_longest_common_collation_prefix (StardictDict *sd,
u_strFromUTF8 (NULL, 0, &uc2_len, s2, -1, &error);
error = U_ZERO_ERROR;
// Prevent undefined behaviour with VLAs.
if (!uc1_len || !uc2_len)
return 0;
UChar uc1[uc1_len];
UChar uc2[uc2_len];
u_strFromUTF8 (uc1, uc1_len, NULL, s1, -1, &error);
@@ -1323,7 +1370,7 @@ stardict_iterator_get_entry (StardictIterator *sdi)
{
g_return_val_if_fail (STARDICT_IS_ITERATOR (sdi), NULL);
if (!stardict_iterator_is_valid (sdi))
return FALSE;
return NULL;
return stardict_dict_get_entry (sdi->owner, sdi->offset);
}

View File

@@ -108,6 +108,7 @@ GQuark stardict_error_quark (void);
// --- Dictionary information --------------------------------------------------
StardictInfo *stardict_info_new (const gchar *filename, GError **error);
const gchar *stardict_info_get_path (StardictInfo *sdi) G_GNUC_PURE;
const gchar *stardict_info_get_book_name (StardictInfo *sdi) G_GNUC_PURE;
gsize stardict_info_get_word_count (StardictInfo *sd) G_GNUC_PURE;
@@ -198,7 +199,7 @@ struct stardict_entry_field
struct stardict_entry
{
GObject parent_instance;
GList * fields; ///< List of StardictEntryField's
GList * fields; ///< List of StardictEntryField-s
};
struct stardict_entry_class
@@ -209,4 +210,4 @@ struct stardict_entry_class
GType stardict_entry_get_type (void);
const GList *stardict_entry_get_fields (StardictEntry *sde) G_GNUC_PURE;
#endif // ! STARDICT_H
#endif // ! STARDICT_H

View File

@@ -150,7 +150,7 @@ worker_writer (WorkerData *data)
stardict_iterator_next (data->iterator);
if (fprintf (data->child_stdin, "%s\n", x) < 0)
fatal ("write to eSpeak failed: %s\n", strerror (errno));
fatal ("write to eSpeak failed: %s\n", g_strerror (errno));
g_free (x);
}
@@ -250,30 +250,6 @@ worker (WorkerData *data)
// --- Main --------------------------------------------------------------------
/// Copy the contents of one StardictInfo object into another. Ignores path.
static void
stardict_info_copy (StardictInfo *dest, const StardictInfo *src)
{
dest->version = src->version;
guint i;
for (i = 0; i < _stardict_ifo_keys_length; i++)
{
const struct stardict_ifo_key *key = &_stardict_ifo_keys[i];
if (key->type == IFO_STRING)
{
gchar **p = &G_STRUCT_MEMBER (gchar *, dest, key->offset);
gchar *q = G_STRUCT_MEMBER (gchar *, src, key->offset);
g_free (*p);
*p = q ? g_strdup (q) : NULL;
}
else
G_STRUCT_MEMBER (gulong, dest, key->offset) =
G_STRUCT_MEMBER (gulong, src, key->offset);
}
}
int
main (int argc, char *argv[])
{
@@ -308,7 +284,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
fatal ("Error: option parsing failed: %s\n", error->message);
if (argc != 3)
fatal ("%s", g_option_context_get_help (ctx, TRUE, FALSE));
fatal ("%s", g_option_context_get_help (ctx, TRUE, NULL));
g_option_context_free (ctx);

875
src/tdv-gui.c Normal file
View File

@@ -0,0 +1,875 @@
/*
* StarDict GTK+ UI
*
* Copyright (c) 2020 - 2024, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <stdlib.h>
#include "config.h"
#include "stardict.h"
#include "utils.h"
#include "stardict-view.h"
static struct
{
GtkWidget *window; ///< Top-level window
GtkWidget *notebook; ///< Notebook with tabs
GtkWidget *hamburger; ///< Hamburger menu
GtkWidget *entry; ///< Search entry widget
GtkWidget *view; ///< Entries view
gint dictionary; ///< Index of the current dictionary
gint last; ///< The last dictionary index
GPtrArray *dictionaries; ///< All open dictionaries
gboolean loading; ///< Dictionaries are being loaded
gboolean watch_selection; ///< Following X11 PRIMARY?
}
g;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_from_filenames (GPtrArray *out, gchar **filenames)
{
for (gsize i = 0; filenames[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_strdup (filenames[i]);
g_ptr_array_add (out, dict);
}
}
// TODO: try to deduplicate, similar to app_load_config_values()
static gboolean
load_from_key_file (GPtrArray *out, GKeyFile *kf, GError **error)
{
const gchar *dictionaries = "Dictionaries";
gchar **names = g_key_file_get_keys (kf, dictionaries, NULL, NULL);
if (!names)
return TRUE;
for (gsize i = 0; names[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->name = names[i];
g_ptr_array_add (out, dict);
}
g_free (names);
for (gsize i = 0; i < out->len; i++)
{
Dictionary *dict = g_ptr_array_index (out, i);
gchar *path =
g_key_file_get_string (kf, dictionaries, dict->name, error);
if (!path)
return FALSE;
// Try to resolve relative paths and expand tildes
if (!(dict->filename =
resolve_filename (path, resolve_relative_config_filename)))
dict->filename = path;
else
g_free (path);
}
return TRUE;
}
static gboolean
load_from_config (GPtrArray *out, GError **error)
{
GKeyFile *key_file = load_project_config_file (error);
if (!key_file)
return FALSE;
gboolean result = load_from_key_file (out, key_file, error);
g_key_file_free (key_file);
return result;
}
static void
search (Dictionary *dict)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
const gchar *input_utf8 = gtk_entry_buffer_get_text (buf);
if (!dict->dict)
return;
StardictIterator *iterator =
stardict_dict_search (dict->dict, input_utf8, NULL);
stardict_view_set_position (STARDICT_VIEW (g.view),
dict->dict, stardict_iterator_get_offset (iterator));
g_object_unref (iterator);
stardict_view_set_matched (STARDICT_VIEW (g.view), input_utf8);
}
static void
on_changed (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
search (g_ptr_array_index (g.dictionaries, g.dictionary));
}
static void
on_send (G_GNUC_UNUSED StardictView *view,
const char *word, G_GNUC_UNUSED gpointer data)
{
GtkEntryBuffer *buf = gtk_entry_get_buffer (GTK_ENTRY (g.entry));
gtk_entry_buffer_set_text (buf, word, -1);
gtk_editable_select_region (GTK_EDITABLE (g.entry), 0, -1);
}
static void
on_selection_received (G_GNUC_UNUSED GtkClipboard *clipboard, const gchar *text,
G_GNUC_UNUSED gpointer data)
{
if (!text)
return;
gchar *trimmed = g_strstrip (g_strdup (text));
gtk_entry_set_text (GTK_ENTRY (g.entry), trimmed);
g_free (trimmed);
g_signal_emit_by_name (g.entry,
"move-cursor", GTK_MOVEMENT_BUFFER_ENDS, 1, FALSE);
}
static void
on_selection (GtkClipboard *clipboard, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
if (g.watch_selection
&& !gtk_window_has_toplevel_focus (GTK_WINDOW (g.window))
&& event->owner_change.owner != NULL)
gtk_clipboard_request_text (clipboard, on_selection_received, NULL);
}
static void
on_selection_watch_toggle (GtkCheckMenuItem *item, G_GNUC_UNUSED gpointer data)
{
g.watch_selection = gtk_check_menu_item_get_active (item);
}
static void
on_switch_page (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GtkWidget *page,
guint page_num, G_GNUC_UNUSED gpointer data)
{
g.last = g.dictionary;
g.dictionary = page_num;
search (g_ptr_array_index (g.dictionaries, g.dictionary));
// Hack: Make right-clicking notebook arrows also re-focus the entry.
GdkEvent *event = gtk_get_current_event ();
if (event && event->type == GDK_BUTTON_PRESS)
gtk_widget_grab_focus (g.entry);
}
static gboolean
accelerate_hamburger (GdkEvent *event)
{
gchar *accelerator = NULL;
g_object_get (gtk_widget_get_settings (g.window), "gtk-menu-bar-accel",
&accelerator, NULL);
if (!accelerator)
return FALSE;
guint key = 0;
GdkModifierType mods = 0;
gtk_accelerator_parse (accelerator, &key, &mods);
g_free (accelerator);
guint mask = gtk_accelerator_get_default_mod_mask ();
if (!key || event->key.keyval != key || (event->key.state & mask) != mods)
return FALSE;
gtk_button_clicked (GTK_BUTTON (g.hamburger));
return TRUE;
}
static gboolean
on_key_press (G_GNUC_UNUSED GtkWidget *widget, GdkEvent *event,
G_GNUC_UNUSED gpointer data)
{
// The "activate" signal of the GtkMenuButton cannot be used
// from a real accelerator, due to "no trigger event for menu popup".
if (accelerate_hamburger (event))
return TRUE;
GtkNotebook *notebook = GTK_NOTEBOOK (g.notebook);
guint mods = event->key.state & gtk_accelerator_get_default_mod_mask ();
if (mods == GDK_CONTROL_MASK)
{
// Can't use gtk_widget_add_accelerator() to change-current-page(-1/+1)
// because that signal has arguments, which cannot be passed.
gint current = gtk_notebook_get_current_page (notebook);
if (event->key.keyval == GDK_KEY_Page_Up)
return gtk_notebook_set_current_page (notebook, --current), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return gtk_notebook_set_current_page (notebook,
++current % gtk_notebook_get_n_pages (notebook)), TRUE;
}
if (mods == GDK_MOD1_MASK)
{
if (event->key.keyval >= GDK_KEY_0
&& event->key.keyval <= GDK_KEY_9)
{
gint n = event->key.keyval - GDK_KEY_0;
gtk_notebook_set_current_page (notebook, (n ? n : 10) - 1);
return TRUE;
}
if (event->key.keyval == GDK_KEY_Tab)
{
gtk_notebook_set_current_page (notebook, g.last);
return TRUE;
}
}
if (mods == 0)
{
StardictView *view = STARDICT_VIEW (g.view);
if (event->key.keyval == GDK_KEY_Page_Up)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, -0.5), TRUE;
if (event->key.keyval == GDK_KEY_Page_Down)
return stardict_view_scroll (view, GTK_SCROLL_PAGES, +0.5), TRUE;
if (event->key.keyval == GDK_KEY_Up)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, -1), TRUE;
if (event->key.keyval == GDK_KEY_Down)
return stardict_view_scroll (view, GTK_SCROLL_STEPS, +1), TRUE;
}
return FALSE;
}
static gboolean
on_tab_focus (G_GNUC_UNUSED GtkWidget *widget,
G_GNUC_UNUSED GtkDirectionType direction, G_GNUC_UNUSED gpointer user_data)
{
// Hack: Make it so that tab headers don't retain newly gained focus
// when clicked, re-focus the entry instead.
GdkEvent *event = gtk_get_current_event ();
if (!event || event->type != GDK_BUTTON_PRESS
|| event->button.button != GDK_BUTTON_PRIMARY)
return FALSE;
gtk_widget_grab_focus (g.entry);
return TRUE;
}
static void
init_tabs (void)
{
for (gsize i = g.dictionaries->len; i--; )
{
Dictionary *dict = g_ptr_array_index (g.dictionaries, i);
GtkWidget *dummy = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
g_signal_connect (dummy, "focus", G_CALLBACK (on_tab_focus), NULL);
GtkWidget *label = gtk_label_new (dict->name);
gtk_notebook_insert_page (GTK_NOTEBOOK (g.notebook), dummy, label, 0);
}
gtk_widget_show_all (g.notebook);
gtk_widget_grab_focus (g.entry);
}
static void
show_error_dialog (GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (g.window), 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
g_error_free (error);
}
// --- Loading -----------------------------------------------------------------
static void
on_new_dictionaries_loaded (G_GNUC_UNUSED GObject* source_object,
GAsyncResult* res, G_GNUC_UNUSED gpointer user_data)
{
g.loading = FALSE;
GError *error = NULL;
GPtrArray *new_dictionaries =
g_task_propagate_pointer (G_TASK (res), &error);
if (!new_dictionaries)
{
show_error_dialog (error);
return;
}
while (gtk_notebook_get_n_pages (GTK_NOTEBOOK (g.notebook)))
gtk_notebook_remove_page (GTK_NOTEBOOK (g.notebook), -1);
g.dictionary = -1;
if (g.dictionaries)
g_ptr_array_free (g.dictionaries, TRUE);
stardict_view_set_position (STARDICT_VIEW (g.view), NULL, 0);
g.dictionaries = new_dictionaries;
init_tabs ();
}
static void
on_reload_dictionaries_task (GTask *task, G_GNUC_UNUSED gpointer source_object,
gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
GError *error = NULL;
if (load_dictionaries (task_data, &error))
{
g_task_return_pointer (task,
g_ptr_array_ref (task_data), (GDestroyNotify) g_ptr_array_unref);
}
else
g_task_return_error (task, error);
}
static gboolean
reload_dictionaries (GPtrArray *new_dictionaries, GError **error)
{
// TODO: We could cancel that task.
if (g.loading)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"already loading dictionaries");
return FALSE;
}
// TODO: Some other kind of indication.
// Note that "action widgets" aren't visible without GtkNotebook tabs.
g.loading = TRUE;
GTask *task = g_task_new (NULL, NULL, on_new_dictionaries_loaded, NULL);
g_task_set_name (task, __func__);
g_task_set_task_data (task,
new_dictionaries, (GDestroyNotify) g_ptr_array_unref);
g_task_run_in_thread (task, on_reload_dictionaries_task);
g_object_unref (task);
return TRUE;
}
static GtkWidget *
new_open_dialog (void)
{
// The default is local-only. Paths are returned absolute.
GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open dictionary"),
GTK_WINDOW (g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT, NULL);
GtkFileFilter *filter = gtk_file_filter_new ();
gtk_file_filter_add_pattern (filter, "*.ifo");
gtk_file_filter_set_name (filter, "*.ifo");
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
gtk_file_chooser_add_filter (chooser, filter);
gtk_file_chooser_set_select_multiple (chooser, TRUE);
return dialog;
}
static void
on_open (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
{
GSList *paths = gtk_file_chooser_get_filenames (chooser);
for (GSList *iter = paths; iter; iter = iter->next)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = iter->data;
g_ptr_array_add (new_dictionaries, dict);
}
g_slist_free (paths);
}
gtk_widget_destroy (dialog);
GError *error = NULL;
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
if (error)
show_error_dialog (error);
}
static void
on_drag_data_received (G_GNUC_UNUSED GtkWidget *widget, GdkDragContext *context,
G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, GtkSelectionData *data,
G_GNUC_UNUSED guint info, guint time, G_GNUC_UNUSED gpointer user_data)
{
GError *error = NULL;
gchar **dropped_uris = gtk_selection_data_get_uris (data);
if (!dropped_uris)
return;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
for (gsize i = 0; !error && dropped_uris[i]; i++)
{
Dictionary *dict = g_malloc0 (sizeof *dict);
dict->filename = g_filename_from_uri (dropped_uris[i], NULL, &error);
g_ptr_array_add (new_dictionaries, dict);
}
g_strfreev (dropped_uris);
if (!new_dictionaries->len
|| !reload_dictionaries (new_dictionaries, &error))
g_ptr_array_free (new_dictionaries, TRUE);
gtk_drag_finish (context, error == NULL, FALSE, time);
if (error)
show_error_dialog (error);
}
// --- Settings ----------------------------------------------------------------
typedef struct settings_data SettingsData;
enum
{
SETTINGS_COLUMN_NAME,
SETTINGS_COLUMN_PATH,
SETTINGS_COLUMN_COUNT
};
struct settings_data
{
GKeyFile *key_file; ///< Configuration file
GtkTreeModel *model; ///< GtkListStore
};
static void
settings_load (SettingsData *data)
{
// We want to keep original comments, as well as any other data.
GError *error = NULL;
data->key_file = load_project_config_file (&error);
if (!data->key_file)
{
if (error)
show_error_dialog (error);
data->key_file = g_key_file_new ();
}
GtkListStore *list_store = gtk_list_store_new (SETTINGS_COLUMN_COUNT,
G_TYPE_STRING, G_TYPE_STRING);
data->model = GTK_TREE_MODEL (list_store);
const gchar *dictionaries = "Dictionaries";
gchar **names =
g_key_file_get_keys (data->key_file, dictionaries, NULL, NULL);
if (!names)
return;
for (gsize i = 0; names[i]; i++)
{
gchar *path = g_key_file_get_string (data->key_file,
dictionaries, names[i], NULL);
if (!path)
continue;
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, names[i], SETTINGS_COLUMN_PATH, path, -1);
g_free (path);
}
g_strfreev (names);
}
static void
settings_save (SettingsData *data)
{
const gchar *dictionaries = "Dictionaries";
g_key_file_remove_group (data->key_file, dictionaries, NULL);
GtkTreeIter iter = { 0 };
gboolean valid = gtk_tree_model_get_iter_first (data->model, &iter);
while (valid)
{
gchar *name = NULL, *path = NULL;
gtk_tree_model_get (data->model, &iter,
SETTINGS_COLUMN_NAME, &name, SETTINGS_COLUMN_PATH, &path, -1);
if (name && path)
g_key_file_set_string (data->key_file, dictionaries, name, path);
g_free (name);
g_free (path);
valid = gtk_tree_model_iter_next (data->model, &iter);
}
GError *e = NULL;
if (!save_project_config_file (data->key_file, &e))
show_error_dialog (e);
}
static void
on_settings_name_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_NAME, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_path_edited (G_GNUC_UNUSED GtkCellRendererText *cell,
const gchar *path_string, const gchar *new_text, gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL (data);
GtkTreePath *path = gtk_tree_path_new_from_string (path_string);
GtkTreeIter iter = { 0 };
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
SETTINGS_COLUMN_PATH, new_text, -1);
gtk_tree_path_free (path);
}
static void
on_settings_add (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkWidget *dialog = new_open_dialog ();
GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog);
GSList *paths = NULL;
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
paths = gtk_file_chooser_get_filenames (chooser);
gtk_widget_destroy (dialog);
// When the dialog is aborted, we simply add an empty list.
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (tree_view));
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
const gchar *home = g_get_home_dir ();
for (GSList *iter = paths; iter; iter = iter->next)
{
GError *error = NULL;
StardictInfo *ifo = stardict_info_new (iter->data, &error);
g_free (iter->data);
if (!ifo)
{
show_error_dialog (error);
continue;
}
// We also expand tildes, even on Windows, so no problem there.
const gchar *path = stardict_info_get_path (ifo);
gchar *tildified = g_str_has_prefix (stardict_info_get_path (ifo), home)
? g_strdup_printf ("~%s", path + strlen (home))
: g_strdup (path);
GtkTreeIter iter = { 0 };
gtk_list_store_append (list_store, &iter);
gtk_list_store_set (list_store, &iter,
SETTINGS_COLUMN_NAME, stardict_info_get_book_name (ifo),
SETTINGS_COLUMN_PATH, tildified, -1);
g_free (tildified);
stardict_info_free (ifo);
}
g_slist_free (paths);
}
static void
on_settings_remove (G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
GtkTreeView *tree_view = GTK_TREE_VIEW (user_data);
GtkTreeSelection *selection = gtk_tree_view_get_selection (tree_view);
GtkTreeModel *model = gtk_tree_view_get_model (tree_view);
GtkListStore *list_store = GTK_LIST_STORE (model);
GList *selected = gtk_tree_selection_get_selected_rows (selection, &model);
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = iter->data;
iter->data = gtk_tree_row_reference_new (model, path);
gtk_tree_path_free (path);
}
for (GList *iter = selected; iter; iter = iter->next)
{
GtkTreePath *path = gtk_tree_row_reference_get_path (iter->data);
if (path)
{
GtkTreeIter tree_iter = { 0 };
if (gtk_tree_model_get_iter (model, &tree_iter, path))
gtk_list_store_remove (list_store, &tree_iter);
gtk_tree_path_free (path);
}
}
g_list_free_full (selected, (GDestroyNotify) gtk_tree_row_reference_free);
}
static void
on_settings_selection_changed
(GtkTreeSelection* selection, gpointer user_data)
{
GtkWidget *remove = GTK_WIDGET (user_data);
gtk_widget_set_sensitive (remove,
gtk_tree_selection_count_selected_rows (selection) > 0);
}
static void
on_settings (G_GNUC_UNUSED GtkMenuItem *item, G_GNUC_UNUSED gpointer data)
{
SettingsData sd = {};
settings_load (&sd);
GtkWidget *treeview = gtk_tree_view_new_with_model (sd.model);
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (treeview), TRUE);
g_object_unref (sd.model);
GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_name_edited), sd.model);
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes
(_("Name"), renderer, "text", SETTINGS_COLUMN_NAME, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_signal_connect (renderer, "edited",
G_CALLBACK (on_settings_path_edited), sd.model);
column = gtk_tree_view_column_new_with_attributes
(_("Path"), renderer, "text", SETTINGS_COLUMN_PATH, NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
GtkWidget *scrolled = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type
(GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_ETCHED_IN);
gtk_container_add (GTK_CONTAINER (scrolled), treeview);
GtkWidget *dialog = gtk_dialog_new_with_buttons (_("Settings"),
GTK_WINDOW (g.window),
GTK_DIALOG_MODAL,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Save"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 400);
GtkWidget *remove = gtk_button_new_with_mnemonic (_("_Remove"));
gtk_widget_set_sensitive (remove, FALSE);
g_signal_connect (remove, "clicked",
G_CALLBACK (on_settings_remove), treeview);
GtkWidget *add = gtk_button_new_with_mnemonic (_("_Add..."));
g_signal_connect (add, "clicked",
G_CALLBACK (on_settings_add), treeview);
GtkTreeSelection *selection =
gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
g_signal_connect (selection, "changed",
G_CALLBACK (on_settings_selection_changed), remove);
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start (GTK_BOX (box),
gtk_label_new (_("Here you can configure the default dictionaries.")),
FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), remove, FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), add, FALSE, FALSE, 0);
GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
g_object_set (content_area, "margin", 12, NULL);
gtk_box_pack_start (GTK_BOX (content_area), box, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (content_area), scrolled, TRUE, TRUE, 12);
gtk_widget_show_all (dialog);
switch (gtk_dialog_run (GTK_DIALOG (dialog)))
{
case GTK_RESPONSE_NONE:
break;
case GTK_RESPONSE_ACCEPT:
settings_save (&sd);
// Fall through
default:
gtk_widget_destroy (dialog);
}
g_key_file_free (sd.key_file);
}
// --- Main --------------------------------------------------------------------
static void
on_destroy (G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED gpointer data)
{
gtk_main_quit ();
}
static void
die_with_dialog (const gchar *message)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
exit (EXIT_FAILURE);
}
int
gui_main (char *argv[])
{
// Just like with GtkApplication, argv has been parsed by the option group.
gtk_init (NULL, NULL);
gtk_window_set_default_icon_name (PROJECT_NAME);
GError *error = NULL;
GPtrArray *new_dictionaries =
g_ptr_array_new_with_free_func ((GDestroyNotify) dictionary_destroy);
if (argv[0])
load_from_filenames (new_dictionaries, argv);
else if (!load_from_config (new_dictionaries, &error) && error)
die_with_dialog (error->message);
if (!new_dictionaries->len)
{
GtkWidget *dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
_("No dictionaries found either in "
"the configuration or on the command line"));
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
// This is better than nothing.
// Our GtkNotebook action widget would be invisible without any tabs.
on_settings (NULL, NULL);
exit (EXIT_SUCCESS);
}
// Some Adwaita stupidity, plus defaults for our own widget.
// All the named colours have been there since GNOME 3.4
// (see gnome-extra-themes git history, Adwaita used to live there).
const char *style = "notebook header tab { padding: 2px 8px; margin: 0; }"
// `gsettings set org.gnome.desktop.interface gtk-key-theme "Emacs"`
// isn't quite what I want, and note that ^U works by default
"@binding-set Readline {"
"bind '<Control>H' { 'delete-from-cursor' (chars, -1) };"
"bind '<Control>W' { 'delete-from-cursor' (word-ends, -1) }; }"
"entry { -gtk-key-bindings: Readline; border-radius: 0; }"
"stardict-view { padding: 0 .25em; }"
"stardict-view.odd {"
"background: @theme_base_color; "
"color: @theme_text_color; }"
"stardict-view.odd:backdrop {"
"background: @theme_unfocused_base_color; "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view.even {"
"background: mix(@theme_base_color, @theme_text_color, 0.03); "
"color: @theme_text_color; }"
"stardict-view.even:backdrop {"
"background: mix(@theme_unfocused_base_color, "
"@theme_fg_color, 0.03); "
"color: @theme_fg_color; /* should be more faded than 'text' */ }"
"stardict-view:selected {"
"background-color: @theme_selected_bg_color; "
"color: @theme_selected_fg_color; }";
GdkScreen *screen = gdk_screen_get_default ();
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, style, strlen (style), NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GtkWidget *item_open = gtk_menu_item_new_with_mnemonic (_("_Open..."));
g_signal_connect (item_open, "activate", G_CALLBACK (on_open), NULL);
GtkWidget *item_settings = gtk_menu_item_new_with_mnemonic (_("_Settings"));
g_signal_connect (item_settings, "activate",
G_CALLBACK (on_settings), NULL);
g.watch_selection = TRUE;
GtkWidget *item_selection =
gtk_check_menu_item_new_with_mnemonic (_("_Follow selection"));
gtk_check_menu_item_set_active
(GTK_CHECK_MENU_ITEM (item_selection), g.watch_selection);
g_signal_connect (item_selection, "toggled",
G_CALLBACK (on_selection_watch_toggle), NULL);
GtkWidget *menu = gtk_menu_new ();
gtk_widget_set_halign (menu, GTK_ALIGN_END);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_open);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_settings);
#ifndef G_OS_WIN32
gtk_menu_shell_append (GTK_MENU_SHELL (menu), item_selection);
#endif // ! G_OS_WIN32
gtk_widget_show_all (menu);
g.hamburger = gtk_menu_button_new ();
gtk_button_set_relief (GTK_BUTTON (g.hamburger), GTK_RELIEF_NONE);
gtk_button_set_image (GTK_BUTTON (g.hamburger), gtk_image_new_from_icon_name
("open-menu-symbolic", GTK_ICON_SIZE_BUTTON));
gtk_menu_button_set_popup (GTK_MENU_BUTTON (g.hamburger), menu);
gtk_widget_show (g.hamburger);
g.notebook = gtk_notebook_new ();
g_signal_connect (g.notebook, "switch-page",
G_CALLBACK (on_switch_page), NULL);
gtk_notebook_set_scrollable (GTK_NOTEBOOK (g.notebook), TRUE);
gtk_notebook_set_action_widget
(GTK_NOTEBOOK (g.notebook), g.hamburger, GTK_PACK_END);
g.entry = gtk_search_entry_new ();
g_signal_connect (g.entry, "changed", G_CALLBACK (on_changed), g.view);
g.window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (g.window), PROJECT_NAME);
gtk_window_set_default_size (GTK_WINDOW (g.window), 300, 600);
g_signal_connect (g.window, "destroy",
G_CALLBACK (on_destroy), NULL);
g_signal_connect (g.window, "key-press-event",
G_CALLBACK (on_key_press), NULL);
GtkWidget *superbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 1);
gtk_container_add (GTK_CONTAINER (g.window), superbox);
gtk_container_add (GTK_CONTAINER (superbox), g.notebook);
gtk_container_add (GTK_CONTAINER (superbox), g.entry);
gtk_container_add (GTK_CONTAINER (superbox),
gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
g.view = stardict_view_new ();
gtk_box_pack_end (GTK_BOX (superbox), g.view, TRUE, TRUE, 0);
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
g_signal_connect (clipboard, "owner-change",
G_CALLBACK (on_selection), NULL);
gtk_drag_dest_set (g.view,
GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
gtk_drag_dest_add_uri_targets (g.view);
g_signal_connect (g.view, "drag-data-received",
G_CALLBACK (on_drag_data_received), NULL);
g_signal_connect (g.view, "send",
G_CALLBACK (on_send), NULL);
if (!reload_dictionaries (new_dictionaries, &error))
die_with_dialog (error->message);
gtk_widget_show_all (g.window);
gtk_main ();
return 0;
}

313
src/tdv-query-tool.c Normal file
View File

@@ -0,0 +1,313 @@
/*
* A tool to query multiple dictionaries for the specified word
*
* Intended for use in IRC bots and similar silly things---words go in,
* one per each line, and entries come out, one dictionary at a time,
* finalised with an empty line. Newlines are escaped with `\n',
* backslashes with `\\'.
*
* So far only the `m', `g`, and `x` fields are supported, as in tdv.
*
* Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include <gio/gio.h>
#include <pango/pango.h>
#include "stardict.h"
#include "stardict-private.h"
#include "generator.h"
#include "utils.h"
// --- Output formatting -------------------------------------------------------
/// Transform Pango attributes to in-line formatting sequences (non-reentrant)
typedef const gchar *(*FormatterFunc) (PangoAttrIterator *);
static const gchar *
pango_attrs_ignore (G_GNUC_UNUSED PangoAttrIterator *iterator)
{
return "";
}
static const gchar *
pango_attrs_to_irc (PangoAttrIterator *iterator)
{
static gchar buf[5];
gchar *p = buf;
*p++ = 0x0f;
if (!iterator)
goto reset_formatting;
PangoAttrInt *attr = NULL;
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_WEIGHT)) && attr->value >= PANGO_WEIGHT_BOLD)
*p++ = 0x02;
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_UNDERLINE)) && attr->value == PANGO_UNDERLINE_SINGLE)
*p++ = 0x1f;
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_STYLE)) && attr->value == PANGO_STYLE_ITALIC)
*p++ = 0x1d;
reset_formatting:
*p++ = 0;
return buf;
}
static const gchar *
pango_attrs_to_ansi (PangoAttrIterator *iterator)
{
static gchar buf[16];
g_strlcpy (buf, "\x1b[0", sizeof buf);
if (!iterator)
goto reset_formatting;
PangoAttrInt *attr = NULL;
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_WEIGHT)) && attr->value >= PANGO_WEIGHT_BOLD)
g_strlcat (buf, ";1", sizeof buf);
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_UNDERLINE)) && attr->value == PANGO_UNDERLINE_SINGLE)
g_strlcat (buf, ";4", sizeof buf);
if ((attr = (PangoAttrInt *) pango_attr_iterator_get (iterator,
PANGO_ATTR_STYLE)) && attr->value == PANGO_STYLE_ITALIC)
g_strlcat (buf, ";3", sizeof buf);
reset_formatting:
g_strlcat (buf, "m", sizeof buf);
return buf;
}
static gchar *
pango_to_output_text (const gchar *markup, FormatterFunc formatter)
{
// This function skips leading whitespace, but it's the canonical one
gchar *text = NULL;
PangoAttrList *attrs = NULL;
if (!pango_parse_markup (markup, -1, 0, &attrs, &text, NULL, NULL))
return g_strdup_printf ("<%s>", ("error in entry"));
PangoAttrIterator *iterator = pango_attr_list_get_iterator (attrs);
GString *result = g_string_new ("");
do
{
gint start = 0, end = 0;
pango_attr_iterator_range (iterator, &start, &end);
if (end == G_MAXINT)
end = strlen (text);
g_string_append (result, formatter (iterator));
g_string_append_len (result, text + start, end - start);
}
while (pango_attr_iterator_next (iterator));
g_string_append (result, formatter (NULL));
g_free (text);
pango_attr_iterator_destroy (iterator);
pango_attr_list_unref (attrs);
return g_string_free (result, FALSE);
}
static gchar *
field_to_output_text (const StardictEntryField *field, FormatterFunc formatter)
{
const gchar *definition = field->data;
if (field->type == STARDICT_FIELD_MEANING)
return g_strdup (definition);
if (field->type == STARDICT_FIELD_PANGO)
return pango_to_output_text (definition, formatter);
if (field->type == STARDICT_FIELD_XDXF)
{
gchar *markup = xdxf_to_pango_markup_with_reduced_effort (definition);
gchar *result = pango_to_output_text (markup, formatter);
g_free (markup);
return result;
}
return NULL;
}
// --- Main --------------------------------------------------------------------
static guint
count_equal_chars (const gchar *a, const gchar *b)
{
guint count = 0;
while (*a && *b)
if (*a++ == *b++)
count++;
return count;
}
static void
do_dictionary (StardictDict *dict, const gchar *word, FormatterFunc formatter)
{
gboolean found;
StardictIterator *iter = stardict_dict_search (dict, word, &found);
if (!found)
goto out;
// Default Stardict ordering is ASCII case-insensitive,
// which may be further exacerbated by our own collation feature.
// Try to find a better matching entry:
gint64 best_offset = stardict_iterator_get_offset (iter);
guint best_score = count_equal_chars
(stardict_iterator_get_word (iter), word);
while (TRUE)
{
stardict_iterator_next (iter);
if (!stardict_iterator_is_valid (iter))
break;
const gchar *iter_word = stardict_iterator_get_word (iter);
if (g_ascii_strcasecmp (iter_word, word))
break;
guint score = count_equal_chars (iter_word, word);
if (score > best_score)
{
best_offset = stardict_iterator_get_offset (iter);
best_score = score;
}
}
stardict_iterator_set_offset (iter, best_offset, FALSE);
StardictEntry *entry = stardict_iterator_get_entry (iter);
StardictInfo *info = stardict_dict_get_info (dict);
const GList *list = stardict_entry_get_fields (entry);
for (; list; list = list->next)
{
StardictEntryField *field = list->data;
gchar *definitions = field_to_output_text (field, formatter);
if (!definitions)
continue;
printf ("%s\t", info->book_name);
for (const gchar *p = definitions; *p; p++)
{
if (*p == '\\')
printf ("\\\\");
else if (*p == '\n')
printf ("\\n");
else
putchar (*p);
}
putchar ('\n');
g_free (definitions);
}
g_object_unref (entry);
out:
g_object_unref (iter);
}
static FormatterFunc
parse_options (int *argc, char ***argv)
{
GError *error = NULL;
GOptionContext *ctx = g_option_context_new
("DICTIONARY.ifo... - query multiple dictionaries");
gboolean format_with_ansi = FALSE;
gboolean format_with_irc = FALSE;
GOptionEntry entries[] =
{
{ "ansi", 'a', 0, G_OPTION_ARG_NONE, &format_with_ansi,
"Format with ANSI sequences", NULL },
{ "irc", 'i', 0, G_OPTION_ARG_NONE, &format_with_irc,
"Format with IRC codes", NULL },
{ }
};
g_option_context_add_main_entries (ctx, entries, NULL);
if (!g_option_context_parse (ctx, argc, argv, &error))
{
g_printerr ("Error: option parsing failed: %s\n", error->message);
exit (EXIT_FAILURE);
}
if (*argc < 2)
{
g_printerr ("%s\n", g_option_context_get_help (ctx, TRUE, NULL));
exit (EXIT_FAILURE);
}
g_option_context_free (ctx);
if (format_with_ansi)
return pango_attrs_to_ansi;
if (format_with_irc)
return pango_attrs_to_irc;
return pango_attrs_ignore;
}
int
main (int argc, char *argv[])
{
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
if (glib_check_version (2, 36, 0))
g_type_init ();
G_GNUC_END_IGNORE_DEPRECATIONS
FormatterFunc formatter = parse_options (&argc, &argv);
guint n_dicts = argc - 1;
StardictDict **dicts = g_alloca (sizeof *dicts * n_dicts);
guint i;
for (i = 1; i <= n_dicts; i++)
{
GError *error = NULL;
dicts[i - 1] = stardict_dict_new (argv[i], &error);
if (error)
{
g_printerr ("Error: opening dictionary `%s' failed: %s\n",
argv[i], error->message);
exit (EXIT_FAILURE);
}
}
gint c;
do
{
GString *s = g_string_new (NULL);
while ((c = getchar ()) != EOF && c != '\n')
if (c != '\r')
g_string_append_c (s, c);
if (s->len)
for (i = 0; i < n_dicts; i++)
do_dictionary (dicts[i], s->str, formatter);
printf ("\n");
fflush (NULL);
g_string_free (s, TRUE);
}
while (c != EOF);
for (i = 0; i < n_dicts; i++)
g_object_unref (dicts[i]);
return 0;
}

223
src/tdv-tabfile.c Normal file
View File

@@ -0,0 +1,223 @@
/*
* A clean reimplementation of StarDict's tabfile
*
* Copyright (c) 2020 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <locale.h>
#include <glib.h>
#include <gio/gio.h>
#include <pango/pango.h>
#include <unicode/ucol.h>
#include "config.h"
#include "stardict.h"
#include "stardict-private.h"
#include "generator.h"
#include "utils.h"
static gboolean
set_data_error (GError **error, const gchar *message)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, message);
return FALSE;
}
static const gchar escapes[256] = { ['n'] = '\n', ['t'] = '\t', ['\\'] = '\\' };
static gboolean
inplace_unescape (gchar *line, GError **error)
{
gboolean escape = FALSE;
gchar *dest = line;
for (gchar *src = line; *src; src++)
{
if (escape)
{
escape = FALSE;
if (!(*dest++ = escapes[(guchar) *src]))
return set_data_error (error, "unsupported escape");
}
else if (*src == '\\')
escape = TRUE;
else
*dest++ = *src;
}
if (escape)
return set_data_error (error, "trailing escape character");
*dest = 0;
return TRUE;
}
static gboolean
import_line (Generator *generator, gchar *line, gsize len, GError **error)
{
if (!len)
return TRUE;
if (!g_utf8_validate_len (line, len, NULL))
return set_data_error (error, "not valid UTF-8");
gchar *separator = strchr (line, '\t');
if (!separator)
return set_data_error (error, "keyword separator not found");
*separator++ = 0;
if (strchr (line, '\\'))
// The index wouldn't be sorted correctly with our method
return set_data_error (error, "escapes not allowed in keywords");
gchar *newline = strpbrk (separator, "\r\n");
if (newline)
*newline = 0;
if (!inplace_unescape (line, error)
|| !inplace_unescape (separator, error))
return FALSE;
if (generator->info->same_type_sequence
&& *generator->info->same_type_sequence == STARDICT_FIELD_PANGO
&& !pango_parse_markup (separator, -1, 0, NULL, NULL, NULL, error))
return FALSE;
generator_begin_entry (generator);
return generator_write_string (generator, separator, TRUE, error)
&& generator_finish_entry (generator, line, error);
}
static gboolean
transform (FILE *fsorted, Generator *generator, GError **error)
{
gchar *line = NULL;
gsize size = 0, ln = 1;
for (ssize_t read; (read = getline (&line, &size, fsorted)) >= 0; ln++)
if (!import_line (generator, line, read, error))
break;
free (line);
if (ferror (fsorted))
{
g_set_error_literal (error, G_IO_ERROR,
g_io_error_from_errno (errno), g_strerror (errno));
return FALSE;
}
if (!feof (fsorted))
{
// You'll only get good line number output with presorted input!
g_prefix_error (error, "line %zu: ", ln);
return FALSE;
}
return TRUE;
}
static void
validate_collation_locale (const gchar *locale)
{
UErrorCode error = U_ZERO_ERROR;
UCollator *collator = ucol_open (locale, &error);
if (!collator)
fatal ("failed to create a collator for %s: %s\n",
locale, u_errorName (error));
ucol_close (collator);
}
int
main (int argc, char *argv[])
{
// The GLib help includes an ellipsis character, for some reason
(void) setlocale (LC_ALL, "");
GError *error = NULL;
GOptionContext *ctx = g_option_context_new ("output-basename < input");
g_option_context_set_summary (ctx,
"Create a StarDict dictionary from plaintext.");
gboolean pango_markup = FALSE;
StardictInfo template = {};
GOptionEntry entries[] =
{
{ "pango", 'p', 0, G_OPTION_ARG_NONE, &pango_markup,
"Entries use Pango markup", NULL },
{ "book-name", 'b', 0, G_OPTION_ARG_STRING, &template.book_name,
"Set the book name field", "TEXT" },
{ "author", 'a', 0, G_OPTION_ARG_STRING, &template.author,
"Set the author field ", "NAME" },
{ "e-mail", 'e', 0, G_OPTION_ARG_STRING, &template.email,
"Set the e-mail field", "ADDRESS" },
{ "website", 'w', 0, G_OPTION_ARG_STRING, &template.website,
"Set the website field", "LINK" },
{ "description", 'd', 0, G_OPTION_ARG_STRING, &template.description,
"Set the description field (newlines supported)", "TEXT" },
{ "date", 'D', 0, G_OPTION_ARG_STRING, &template.date,
"Set the date field", "DATE" },
{ "collation", 'c', 0, G_OPTION_ARG_STRING, &template.collation,
"Set the collation field (for ICU)", "LOCALE" },
{ }
};
g_option_context_add_main_entries (ctx, entries, GETTEXT_PACKAGE);
if (!g_option_context_parse (ctx, &argc, &argv, &error))
fatal ("Error: option parsing failed: %s\n", error->message);
if (argc != 2)
fatal ("%s", g_option_context_get_help (ctx, TRUE, NULL));
g_option_context_free (ctx);
template.version = SD_VERSION_3_0_0;
template.same_type_sequence = pango_markup
? (char[]) { STARDICT_FIELD_PANGO, 0 }
: (char[]) { STARDICT_FIELD_MEANING, 0 };
if (!template.book_name)
template.book_name = argv[1];
if (template.description)
{
gchar **lines = g_strsplit (template.description, "\n", -1);
g_free (template.description);
gchar *in_one_line = g_strjoinv ("<br>", lines);
g_strfreev (lines);
template.description = in_one_line;
}
if (template.collation)
validate_collation_locale (template.collation);
// This actually implements stardict_strcmp(), POSIX-compatibly.
// Your sort(1) is not expected to be stable by default, like bsdsort is.
FILE *fsorted = popen ("LC_ALL=C sort -t'\t' -k1f,1", "r");
if (!fsorted)
fatal ("%s: %s\n", "popen", g_strerror (errno));
Generator *generator = generator_new (argv[1], &error);
if (!generator)
fatal ("Error: failed to create the output dictionary: %s\n",
error->message);
StardictInfo *info = generator->info;
stardict_info_copy (info, &template);
if (!transform (fsorted, generator, &error)
|| !generator_finish (generator, &error))
fatal ("Error: failed to write the dictionary: %s\n", error->message);
generator_free (generator);
pclose (fsorted);
return 0;
}

View File

@@ -3,7 +3,7 @@
*
* The external filter needs to process NUL-separated textual entries.
*
* Example: transform input.info output -- perl -p0e s/bullshit/soykaf/g
* Example: tdv-transform input.ifo output -- perl -p0e s/bullshit/soykaf/g
*
* Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
*
@@ -76,8 +76,8 @@ write_to_filter (StardictDict *dict, gint fd, GError **error)
if (write (fd, field->data, field->data_size)
!= (ssize_t) field->data_size)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"%s", strerror (errno));
g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno),
"%s", g_strerror (errno));
return FALSE;
}
}
@@ -117,7 +117,7 @@ update_from_filter (StardictDict *dict, Generator *generator,
gchar *end = memchr (filtered, 0, filtered_end - filtered);
if (!end)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
g_set_error (error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT,
"filter seems to have ended too early");
return FALSE;
}
@@ -140,31 +140,6 @@ update_from_filter (StardictDict *dict, Generator *generator,
return TRUE;
}
// FIXME: copied from add-pronunciation.c, should merge it somewhere (utils?)
/// Copy the contents of one StardictInfo object into another. Ignores path.
static void
stardict_info_copy (StardictInfo *dest, const StardictInfo *src)
{
dest->version = src->version;
guint i;
for (i = 0; i < _stardict_ifo_keys_length; i++)
{
const struct stardict_ifo_key *key = &_stardict_ifo_keys[i];
if (key->type == IFO_STRING)
{
gchar **p = &G_STRUCT_MEMBER (gchar *, dest, key->offset);
gchar *q = G_STRUCT_MEMBER (gchar *, src, key->offset);
g_free (*p);
*p = q ? g_strdup (q) : NULL;
}
else
G_STRUCT_MEMBER (gulong, dest, key->offset) =
G_STRUCT_MEMBER (gulong, src, key->offset);
}
}
int
main (int argc, char *argv[])
{
@@ -176,12 +151,11 @@ main (int argc, char *argv[])
("input.ifo output-basename -- FILTER [ARG...]");
g_option_context_set_summary
(ctx, "Transform dictionaries using a filter program.");
g_option_context_set_description (ctx, "Test?");
if (!g_option_context_parse (ctx, &argc, &argv, &error))
fatal ("Error: option parsing failed: %s\n", error->message);
if (argc < 3)
fatal ("%s", g_option_context_get_help (ctx, TRUE, FALSE));
fatal ("%s", g_option_context_get_help (ctx, TRUE, NULL));
// GLib is bullshit, getopt_long() always correctly removes this
gint program_argv_start = 3;
@@ -202,7 +176,7 @@ main (int argc, char *argv[])
FILE *child_out = tmpfile ();
if (!child_out)
fatal ("tmpfile: %s\n", strerror (errno));
fatal ("tmpfile: %s\n", g_strerror (errno));
GPid pid = -1;
if (!g_spawn_async_with_fds (NULL /* working_directory */,
@@ -221,7 +195,7 @@ main (int argc, char *argv[])
int wstatus = errno = 0;
if (waitpid (pid, &wstatus, 0) < 1
|| !WIFEXITED (wstatus) || WEXITSTATUS (wstatus) > 0)
fatal ("Filter failed (%s, status %d)\n", strerror (errno), wstatus);
fatal ("Filter failed (%s, status %d)\n", g_strerror (errno), wstatus);
GMappedFile *filtered = g_mapped_file_new_from_fd (fileno (child_out),
FALSE /* writable */, &error);

File diff suppressed because it is too large Load Diff

98
src/tdv.c Normal file
View File

@@ -0,0 +1,98 @@
/*
* Translation dictionary viewer
*
* Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include "config.h"
#include <glib.h>
#include <glib/gi18n.h>
#ifdef WITH_GUI
#include <gtk/gtk.h>
#endif
#include <locale.h>
#ifndef G_OS_WIN32
#include <unistd.h>
#endif
int tui_main (char *[]);
int gui_main (char *[]);
int
main (int argc, char *argv[])
{
if (!setlocale (LC_ALL, ""))
g_printerr ("%s: %s\n", _("Warning"), _("failed to set the locale"));
bindtextdomain (GETTEXT_PACKAGE, GETTEXT_DIRNAME);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
gboolean show_version = FALSE;
#ifdef WITH_GUI
# ifndef G_OS_WIN32
gboolean gui = FALSE;
# endif
#endif
GOptionEntry entries[] =
{
{ "version", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &show_version,
N_("Output version information and exit"), NULL },
#ifdef WITH_GUI
# ifndef G_OS_WIN32
{ "gui", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &gui,
N_("Launch the GUI even when run from a terminal"), NULL },
# endif
#endif
{ },
};
GOptionContext *ctx = g_option_context_new
(N_("[dictionary.ifo...] - Translation dictionary viewer"));
g_option_context_add_main_entries (ctx, entries, GETTEXT_PACKAGE);
#ifdef WITH_GUI
g_option_context_add_group (ctx, gtk_get_option_group (FALSE));
#endif
g_option_context_set_translation_domain (ctx, GETTEXT_PACKAGE);
GError *error = NULL;
if (!g_option_context_parse (ctx, &argc, &argv, &error))
{
g_printerr ("%s: %s: %s\n", _("Error"), _("option parsing failed"),
error->message);
exit (EXIT_FAILURE);
}
g_option_context_free (ctx);
if (show_version)
{
g_print (PROJECT_NAME " " PROJECT_VERSION "\n");
exit (EXIT_SUCCESS);
}
#ifdef WITH_GUI
# ifndef G_OS_WIN32
if (gui || !isatty (STDIN_FILENO))
# endif
return gui_main (argv + 1);
#endif
#ifndef G_OS_WIN32
return tui_main (argv + 1);
#endif
}

View File

@@ -1,7 +1,7 @@
/*
* utils.c: miscellaneous utilities
*
* Copyright (c) 2013 - 2020, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -16,22 +16,49 @@
*
*/
// getpwnam_r, _SC_GETPW_R_SIZE_MAX
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#include <glib.h>
#include <glib/gprintf.h>
#include <gio/gio.h>
#include <glib/gstdio.h>
#include <stdlib.h>
#include <errno.h>
#include <stdarg.h>
#include <curses.h>
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
#ifndef WIN32
#include <pwd.h>
#endif // ! WIN32
#include "config.h"
#include "utils.h"
/// Trivially filter out all tags that aren't part of the Pango markup language,
/// or no frontend can quite handle--this seems to work well.
/// Given the nature of our display, also skip whole keyword elements.
gchar *
xdxf_to_pango_markup_with_reduced_effort (const gchar *xml)
{
GString *filtered = g_string_new ("");
while (*xml)
{
// GMarkup can read some of the wilder XML constructs, Pango skips them
const gchar *p = NULL;
if (*xml != '<' || xml[1] == '!' || xml[1] == '?'
|| g_ascii_isspace (xml[1]) || !*(p = xml + 1 + (xml[1] == '/'))
|| (strchr ("biu", *p) && p[1] == '>') || !(p = strchr (p, '>')))
g_string_append_c (filtered, *xml++);
else if (xml[1] != 'k' || xml[2] != '>' || !(xml = strstr (p, "</k>")))
xml = ++p;
}
return g_string_free (filtered, FALSE);
}
/// Read the whole stream into a byte array.
gboolean
stream_read_all (GByteArray *ba, GInputStream *is, GError **error)
@@ -79,35 +106,224 @@ xstrtoul (unsigned long *out, const char *s, int base)
return errno == 0 && !*end && end != s;
}
// Didn't want to have this ugly piece of code in the main source file;
// the standard endwin/refresh sequence makes the terminal flicker.
void
update_curses_terminal_size (void)
{
#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
struct winsize size;
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
{
char *row = getenv ("LINES");
char *col = getenv ("COLUMNS");
unsigned long tmp;
resizeterm (
(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
}
#else // HAVE_RESIZETERM && TIOCGWINSZ
endwin ();
refresh ();
#endif // HAVE_RESIZETERM && TIOCGWINSZ
}
/// Print a fatal error message and terminate the process immediately.
void
fatal (const gchar *format, ...)
{
va_list ap;
va_start (ap, format);
vfprintf (stderr, format, ap);
g_vfprintf (stderr, format, ap);
exit (EXIT_FAILURE);
va_end (ap);
}
// At times, GLib even with its sheer size is surprisingly useless,
// and I need to port some code over from "liberty".
static const gchar **
get_xdg_config_dirs (void)
{
GPtrArray *paths = g_ptr_array_new ();
g_ptr_array_add (paths, (gpointer) g_get_user_config_dir ());
for (const gchar *const *system = g_get_system_config_dirs ();
*system; system++)
g_ptr_array_add (paths, (gpointer) *system);
g_ptr_array_add (paths, NULL);
return (const gchar **) g_ptr_array_free (paths, FALSE);
}
gchar *
resolve_relative_filename_generic
(const gchar **paths, const gchar *tail, const gchar *filename)
{
for (; *paths; paths++)
{
// As per XDG spec, relative paths are ignored
if (**paths != '/')
continue;
gchar *file = g_build_filename (*paths, tail, filename, NULL);
GStatBuf st;
if (!g_stat (file, &st))
return file;
g_free (file);
}
return NULL;
}
gchar *
resolve_relative_config_filename (const gchar *filename)
{
const gchar **paths = get_xdg_config_dirs ();
gchar *result =
resolve_relative_filename_generic (paths, PROJECT_NAME, filename);
g_free (paths);
return result;
}
static gchar *
try_expand_tilde (const gchar *filename)
{
size_t until_slash = strcspn (filename, "/");
if (!until_slash)
return g_build_filename (g_get_home_dir () ?: "", filename, NULL);
#ifdef WIN32
// TODO: also ensure that path separators are handled sensibly around here
return NULL;
#else // ! WIN32
long buf_len = sysconf (_SC_GETPW_R_SIZE_MAX);
if (buf_len < 0)
buf_len = 1024;
struct passwd pwd, *success = NULL;
gchar *user = g_strndup (filename, until_slash);
gchar *buf = g_malloc (buf_len);
while (getpwnam_r (user, &pwd, buf, buf_len, &success) == ERANGE)
buf = g_realloc (buf, buf_len <<= 1);
g_free (user);
gchar *result = NULL;
if (success)
result = g_strdup_printf ("%s%s", pwd.pw_dir, filename + until_slash);
g_free (buf);
return result;
#endif // ! WIN32
}
gchar *
resolve_filename (const gchar *filename, gchar *(*relative_cb) (const char *))
{
// Absolute path is absolute
if (*filename == '/')
return g_strdup (filename);
// We don't want to use wordexp() for this as it may execute /bin/sh
if (*filename == '~')
{
// Paths to home directories ought to be absolute
char *expanded = try_expand_tilde (filename + 1);
if (expanded)
return expanded;
g_debug ("failed to expand the home directory in `%s'", filename);
}
return relative_cb (filename);
}
GKeyFile *
load_project_config_file (GError **error)
{
GKeyFile *key_file = g_key_file_new ();
const gchar **paths = get_xdg_config_dirs ();
GError *e = NULL;
// XXX: if there are dashes in the final path component,
// the function tries to replace them with directory separators,
// which is completely undocumented
g_key_file_load_from_dirs (key_file,
PROJECT_NAME G_DIR_SEPARATOR_S PROJECT_NAME ".conf",
paths, NULL, G_KEY_FILE_KEEP_COMMENTS, &e);
g_free (paths);
if (!e)
return key_file;
if (e->code == G_KEY_FILE_ERROR_NOT_FOUND)
g_error_free (e);
else
g_propagate_error (error, e);
g_key_file_free (key_file);
return NULL;
}
gboolean
save_project_config_file (GKeyFile *key_file, GError **error)
{
gchar *dirname =
g_build_filename (g_get_user_config_dir (), PROJECT_NAME, NULL);
(void) g_mkdir_with_parents (dirname, 0755);
gchar *path = g_build_filename (dirname, PROJECT_NAME ".conf", NULL);
g_free (dirname);
gsize length = 0;
gchar *data = g_key_file_to_data (key_file, &length, error);
if (!data)
return FALSE;
gboolean result = g_file_set_contents (path, data, length, error);
g_free (data);
return result;
}
// --- Loading -----------------------------------------------------------------
void
dictionary_destroy (Dictionary *self)
{
g_free (self->name);
g_free (self->filename);
if (self->dict)
g_object_unref (self->dict);
g_free (self);
}
static gboolean
dictionary_load (Dictionary *self, GError **e)
{
if (!(self->dict = stardict_dict_new (self->filename, e)))
return FALSE;
if (!self->name)
{
self->name = g_strdup (stardict_info_get_book_name
(stardict_dict_get_info (self->dict)));
}
return TRUE;
}
static gboolean
load_dictionaries_sequentially (GPtrArray *dictionaries, GError **e)
{
for (guint i = 0; i < dictionaries->len; i++)
if (!dictionary_load (g_ptr_array_index (dictionaries, i), e))
return FALSE;
return TRUE;
}
// Parallelize dictionary loading if possible, because of collation reindexing
static void
load_worker (gpointer data, gpointer user_data)
{
GError *e = NULL;
dictionary_load (data, &e);
if (e)
g_async_queue_push (user_data, e);
}
gboolean
load_dictionaries (GPtrArray *dictionaries, GError **e)
{
GAsyncQueue *error_queue =
g_async_queue_new_full ((GDestroyNotify) g_error_free);
GThreadPool *pool = g_thread_pool_new (load_worker, error_queue,
g_get_num_processors (), TRUE, NULL);
if G_UNLIKELY (!g_thread_pool_get_num_threads (pool))
{
g_thread_pool_free (pool, TRUE, TRUE);
g_async_queue_unref (error_queue);
return load_dictionaries_sequentially (dictionaries, e);
}
for (guint i = 0; i < dictionaries->len; i++)
g_thread_pool_push (pool, g_ptr_array_index (dictionaries, i), NULL);
g_thread_pool_free (pool, FALSE, TRUE);
gboolean result = TRUE;
if ((*e = g_async_queue_try_pop (error_queue)))
result = FALSE;
g_async_queue_unref (error_queue);
return result;
}

View File

@@ -1,7 +1,7 @@
/*
* utils.h: miscellaneous utilities
*
* Copyright (c) 2013 - 2020, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2013 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -19,6 +19,11 @@
#ifndef UTILS_H
#define UTILS_H
#include <glib.h>
#include <gio/gio.h>
#include "stardict.h"
/// After this statement, the element has been found and its index is stored
/// in the variable "imid".
#define BINARY_SEARCH_BEGIN(max, compare) \
@@ -36,10 +41,33 @@
} \
}
gchar *xdxf_to_pango_markup_with_reduced_effort (const gchar *xml);
gboolean stream_read_all (GByteArray *ba, GInputStream *is, GError **error);
gchar *stream_read_string (GDataInputStream *dis, GError **error);
gboolean xstrtoul (unsigned long *out, const char *s, int base);
void update_curses_terminal_size (void);
void fatal (const gchar *format, ...) G_GNUC_PRINTF (1, 2) G_GNUC_NORETURN;
gchar *resolve_relative_filename_generic
(const gchar **paths, const gchar *tail, const gchar *filename);
gchar *resolve_relative_config_filename (const gchar *filename);
gchar *resolve_filename
(const gchar *filename, gchar *(*relative_cb) (const char *));
GKeyFile *load_project_config_file (GError **error);
gboolean save_project_config_file (GKeyFile *key_file, GError **error);
// --- Loading -----------------------------------------------------------------
typedef struct dictionary Dictionary;
struct dictionary
{
gchar *filename; ///< Path to the dictionary
StardictDict *dict; ///< StarDict dictionary data
gchar *name; ///< Name to show
};
void dictionary_destroy (Dictionary *self);
gboolean load_dictionaries (GPtrArray *dictionaries, GError **e);
#endif // ! UTILS_H

9
tdv.desktop Normal file
View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=tdv
GenericName=Translation dictionary viewer
Icon=tdv
Exec=tdv %F
StartupNotify=true
MimeType=application/x-stardict-ifo;
Categories=Office;Dictionary;GTK;

BIN
tdv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

51
tdv.svg Normal file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
color-interpolation="linearRGB"
color-interpolation-filters="linearRGB">
<defs>
<rect id="text" width="13" height="3" fill="#d0d0d0" />
<filter id="shadow" x="-25%" y="-25%" width="150%" height="150%">
<feFlood flood-color="#000000" flood-opacity=".5" result="flood" />
<feComposite in="SourceGraphic" in2="flood" operator="in" />
<feGaussianBlur stdDeviation="1.25" />
<feOffset dx="1" dy="1" result="offset" />
<feComposite in="SourceGraphic" in2="offset" />
</filter>
<mask id="hole">
<rect x="-25%" y="-25%" width="150%" height="150%" fill="#ffffff" />
<circle r="10.5" />
</mask>
</defs>
<rect x="5" y="1" width="38" height="46" ry="2"
fill="#ffffff" stroke="#606060" stroke-width="1.25" />
<use xlink:href="#text" x="9" y="5" />
<use xlink:href="#text" x="9" y="10" />
<use xlink:href="#text" x="9" y="15" />
<use xlink:href="#text" x="9" y="20" />
<use xlink:href="#text" x="9" y="25" />
<use xlink:href="#text" x="9" y="30" />
<use xlink:href="#text" x="9" y="35" />
<use xlink:href="#text" x="9" y="40" />
<use xlink:href="#text" x="26" y="5" />
<use xlink:href="#text" x="26" y="10" />
<use xlink:href="#text" x="26" y="15" />
<use xlink:href="#text" x="26" y="20" />
<use xlink:href="#text" x="26" y="25" />
<use xlink:href="#text" x="26" y="30" />
<use xlink:href="#text" x="26" y="35" />
<use xlink:href="#text" x="26" y="40" />
<circle cx="21" cy="19" r="9" fill="#ffffff" fill-opacity=".5"
stroke-width="2" stroke="#000000" filter="url(#shadow)" />
<g filter="url(#shadow)">
<rect x="-1.75" y="0" width="3.5" height="22.5"
transform="translate(21 19) rotate(-30)" mask="url(#hole)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

10
tdv.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-stardict-ifo">
<comment>StarDict dictionary main file</comment>
<magic>
<match type="string" offset="0" value="StarDict's dict ifo file"/>
</magic>
<glob pattern="*.ifo"/>
</mime-type>
</mime-info>

2
termo

Submodule termo updated: 30e0eee1a8...f9a102456f