├── .drone.yml
├── .github
├── CODEOWNERS
└── workflows
│ ├── build.yaml
│ ├── release.yaml
│ └── test.yaml
├── .gitignore
├── .gitmodules
├── LICENSE
├── Makefile
├── README.md
├── res
└── doc
├── src
├── action.c
├── action.h
├── action_c.c
├── action_cc.cc
├── config.c
├── config.h
├── generator.c
├── generator.h
├── generator_c.c
├── kfmon.c
├── kfmon.h
├── kfmon_helpers.h
├── nickelmenu.cc
├── nickelmenu.h
├── util.c
└── util.h
└── test
└── syms
├── go.mod
├── go.sum
└── main.go
/.drone.yml:
--------------------------------------------------------------------------------
1 | kind: pipeline
2 | type: docker
3 | name: NickelMenu
4 |
5 | trigger:
6 | event: [push, pull_request, tag]
7 |
8 | steps:
9 | - name: submodules
10 | image: ghcr.io/pgaskin/nickeltc:1.0
11 | when:
12 | event: [push, pull_request, tag]
13 | command: ["git", "submodule", "update", "--init", "--recursive"]
14 | - name: build
15 | image: ghcr.io/pgaskin/nickeltc:1.0
16 | when:
17 | event: [push, pull_request, tag]
18 | commands:
19 | - make clean
20 | - make all koboroot
21 | - mkdir out && mv KoboRoot.tgz src/libnm.so out/
22 | depends_on: [submodules]
23 | - name: build-NM_UNINSTALL_CONFIGDIR
24 | image: ghcr.io/pgaskin/nickeltc:1.0
25 | when:
26 | event: [push, pull_request, tag]
27 | commands:
28 | - make clean
29 | - make all koboroot NM_UNINSTALL_CONFIGDIR=1
30 | - mkdir out/with-NM_UNINSTALL_CONFIGDIR && mv KoboRoot.tgz src/libnm.so out/with-NM_UNINSTALL_CONFIGDIR/
31 | depends_on: [build]
32 | - name: test-syms
33 | image: golang:1.14
34 | when:
35 | event: [push, pull_request, tag]
36 | commands:
37 | - cd test/syms && go build -o ../../test.syms . && cd ../..
38 | - cd src && ../test.syms && cd ..
39 | - name: upload-build
40 | image: plugins/s3
41 | when:
42 | event: [push]
43 | settings:
44 | endpoint: https://s3.geek1011.net
45 | bucket: nickelmenu
46 | access_key: nickelmenu
47 | secret_key: {from_secret: S3_SECRET_NICKELMENU}
48 | target: artifacts/build/${DRONE_BUILD_NUMBER}
49 | source: out/**/*
50 | strip_prefix: out/
51 | depends_on: [build, build-NM_UNINSTALL_CONFIGDIR]
52 | - name: upload-tag
53 | image: plugins/s3
54 | when:
55 | event: [tag]
56 | settings:
57 | endpoint: https://s3.geek1011.net
58 | bucket: nickelmenu
59 | access_key: nickelmenu
60 | secret_key: {from_secret: S3_SECRET_NICKELMENU}
61 | target: artifacts/tag/${DRONE_TAG}
62 | source: out/**/*
63 | strip_prefix: out/
64 | depends_on: [build, build-NM_UNINSTALL_CONFIGDIR]
65 | - name: upload-commit
66 | image: plugins/s3
67 | when:
68 | event: [push]
69 | settings:
70 | endpoint: https://s3.geek1011.net
71 | bucket: nickelmenu
72 | access_key: nickelmenu
73 | secret_key: {from_secret: S3_SECRET_NICKELMENU}
74 | target: artifacts/commit/${DRONE_COMMIT}
75 | source: out/**/*
76 | strip_prefix: out/
77 | depends_on: [build, build-NM_UNINSTALL_CONFIGDIR]
78 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @pgaskin
2 | src/kfmon* @NiLuJe
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | name: NickelMenu
7 | runs-on: ubuntu-latest
8 | container: ghcr.io/pgaskin/nickeltc:1.0
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | with:
13 | submodules: true
14 | - name: Build
15 | run: make all koboroot
16 | - name: Upload
17 | uses: actions/upload-artifact@v4
18 | with:
19 | name: NickelMenu
20 | path: KoboRoot.tgz
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on: {push: {tags: [v*]}}
3 |
4 | jobs:
5 | build:
6 | name: NickelMenu
7 | runs-on: ubuntu-latest
8 | container: ghcr.io/pgaskin/nickeltc:1.0
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | with:
13 | submodules: true
14 | - name: Build
15 | run: make all koboroot
16 | - name: Release
17 | id: release
18 | uses: actions/create-release@latest
19 | env:
20 | GITHUB_TOKEN: ${{secrets.GHTOKEN}}
21 | with:
22 | tag_name: ${{github.ref}}
23 | release_name: NickelMenu ${{github.ref}}
24 | body: ''
25 | draft: true
26 | prerelease: false
27 | - name: Upload
28 | uses: actions/upload-release-asset@v1
29 | env:
30 | GITHUB_TOKEN: ${{secrets.GHTOKEN}}
31 | with:
32 | upload_url: ${{steps.release.outputs.upload_url}}
33 | asset_path: ./KoboRoot.tgz
34 | asset_name: KoboRoot.tgz
35 | asset_content_type: application/gzip
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | name: NickelMenu / Symbols
7 | runs-on: ubuntu-latest
8 | container: docker.io/golang:1.14
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | with:
13 | submodules: true
14 | - name: Build
15 | run: cd test/syms && go build -o ../../test.syms .
16 | - name: Run
17 | run: cd src && ../test.syms
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # make gitignore
2 | .kdev4/
3 | *.kdev4
4 | .kateconfig
5 | .vscode/
6 | .idea/
7 | .clangd/
8 | .cache/
9 | compile_commands.json
10 | /KoboRoot.tgz
11 | /src/libnm.so
12 | /src/action.o
13 | /src/action_c.o
14 | /src/config.o
15 | /src/generator.o
16 | /src/generator_c.o
17 | /src/kfmon.o
18 | /src/util.o
19 | /src/action_cc.o
20 | /src/nickelmenu.o
21 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "NickelHook"]
2 | path = NickelHook
3 | url = https://github.com/pgaskin/NickelHook.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2023 Patrick Gaskin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | include NickelHook/NickelHook.mk
2 |
3 | override PKGCONF += Qt5Widgets
4 | override LIBRARY := src/libnm.so
5 | override SOURCES += src/action.c src/action_c.c src/action_cc.cc src/config.c src/generator.c src/generator_c.c src/kfmon.c src/nickelmenu.cc src/util.c
6 | override CFLAGS += -Wall -Wextra -Werror -fvisibility=hidden
7 | override CXXFLAGS += -Wall -Wextra -Werror -Wno-missing-field-initializers -isystemlib -fvisibility=hidden -fvisibility-inlines-hidden
8 | override KOBOROOT += res/doc:$(NM_CONFIG_DIR)/doc
9 |
10 | override SKIPCONFIGURE += strip
11 | strip:
12 | $(STRIP) --strip-unneeded src/libnm.so
13 | .PHONY: strip
14 |
15 | ifeq ($(NM_UNINSTALL_CONFIGDIR),1)
16 | override CPPFLAGS += -DNM_UNINSTALL_CONFIGDIR
17 | endif
18 |
19 | ifeq ($(NM_CONFIG_DIR),)
20 | override NM_CONFIG_DIR := /mnt/onboard/.adds/nm
21 | endif
22 |
23 | ifneq ($(NM_CONFIG_DIR),/mnt/onboard/.adds/nm)
24 | $(info -- Warning: NM_CONFIG_DIR is set to a non-default value; this will cause issues with other mods using it!)
25 | endif
26 |
27 | override CPPFLAGS += -DNM_CONFIG_DIR='"$(NM_CONFIG_DIR)"' -DNM_CONFIG_DIR_DISP='"$(patsubst /mnt/onboard/%,KOBOeReader/%,$(NM_CONFIG_DIR))"'
28 |
29 | include NickelHook/NickelHook.mk
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
NickelMenu
2 |
3 | The easiest way to launch custom scripts, change hidden settings, and run actions on Kobo eReaders.
4 |
5 | See the [website](https://pgaskin.net/NickelMenu) and [thread on MobileRead](https://mobileread.com/forums/showthread.php?t=329525) for screenshots and more details.
6 |
7 | ## Installation
8 | You can download pre-built packages of the latest stable release from the [releases](https://github.com/pgaskin/NickelMenu/releases) page, or you can find bleeding-edge builds of each commit from [here](https://github.com/pgaskin/NickelMenu/actions).
9 |
10 | After you download the package, copy `KoboRoot.tgz` into the `.kobo` folder of your eReader, then eject it.
11 |
12 | After it installs, you will find a new menu item named `NickelMenu` with further instructions which you can also read [here](./res/doc).
13 |
14 | To uninstall NickelMenu, just create a new file named `uninstall` in `.adds/nm/`, or trigger the failsafe mechanism by immediately powering off the Kobo after it starts booting.
15 |
16 | Most errors, if any, will be displayed as a menu item in the main menu. If no new menu entries appear here after a reboot, try reinstalling NickelMenu. If that still doesn't work, connect over telnet or SSH and check the output of `logread`.
17 |
18 | ## Compiling
19 |
20 | NickelMenu is designed to be compiled with [NickelTC](https://github.com/pgaskin/NickelTC). To compile it with Docker/Podman, use `docker run --volume="$PWD:$PWD" --user="$(id --user):$(id --group)" --workdir="$PWD" --env=HOME --entrypoint=make --rm -it ghcr.io/pgaskin/nickeltc:1.0 all koboroot`. To compile it on the host, use `make CROSS_COMPILE=/path/to/nickeltc/bin/arm-nickel-linux-gnueabihf-`.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/res/doc:
--------------------------------------------------------------------------------
1 | # NickelMenu (libnm.so)
2 | # https://pgaskin.net/NickelMenu
3 | #
4 | # This tool injects menu items into Nickel.
5 | #
6 | # It should work on firmware 4.6+, but it has only been tested on 4.20.14622 -
7 | # 4.31.19086. It is perfectly safe to try out on any newer firmware version, as
8 | # it has a lot of error checking, and a failsafe mechanism which automatically
9 | # uninstalls it as a last resort.
10 | #
11 | # Place your configuration files in this folder. They can be named anything, and
12 | # should consist of multiple lines either starting with # for a comment, or in
13 | # one of the the following formats (spaces around fields are ignored):
14 | #
15 | # menu_item::::
16 | # Adds a menu item.
17 | #
18 | # the menu to add the item to, one of:
19 | # main - the menu in the top-left corner of the home screen
20 | # (in firmware 4.23.15505+, this menu was removed, so a new item will be added to the tabs on the bottom-right)
21 | # reader - the overflow menu in the reader
22 | # browser - the menu in the bottom-right of the web browser
23 | # library - the menu in the filter bar for the "My Books" and "My Articles" library views
24 | # selection - the selection menu (4.20.14622+)
25 | # all arguments will support substitutions in the form {A|B|C} as follows (see the end of this file for examples):
26 | # A is the string to substitute
27 | # 1 - current selection
28 | # B is zero or more transformations applied from left to right:
29 | # a - all to lowercase
30 | # A - all to uppercase
31 | # f - keep everything up to the first whitespace character
32 | # n - remove non-alphanumeric
33 | # s - trim whitespace
34 | # S - normalize whitespace (trim, then collapse multiple whitespace chars)
35 | # u - replace whitespace with underscores
36 | # w - remove all whitespace
37 | # x - special: return an error if the string is empty at this point
38 | # C is zero or more escape methods applied from left to right:
39 | # " - escape for double-quoted strings (JSON-compatible, C-compatible):
40 | # " => \"
41 | # newline => \n
42 | # backspace => \b
43 | # tab => \t
44 | # form-feed => \f
45 | # carriage-return => \r
46 | # \ => \\
47 | # $ - escape for single-quoted strings (bash-compatible):
48 | # ' => '"'"'
49 | # % - url encode all non-alphanumeric chars
50 | # selection_search - the search sub-menu of the selection menu (4.20.14622+)
51 | # see above
52 | # the label to show on the menu item (must not contain :)
53 | # the type of action to run, one of:
54 | # cmd_spawn - starts a command in the background
55 | # cmd_output - runs a command, waits for it to exit, and optionally displays the output
56 | # dbg_syslog - writes a message to syslog (for testing)
57 | # dbg_error - always returns an error (for testing)
58 | # dbg_msg - shows a message (for testing)
59 | # dbg_toast - shows a toast (for testing)
60 | # kfmon - triggers a kfmon action
61 | # nickel_setting - changes a setting
62 | # nickel_extras - opens one of the beta features
63 | # nickel_browser - opens the browser
64 | # (on firmware 4.23.15505+, you won't be able to exit the browser unless you use the "modal" option or add "menu_item:browser:Quit:nickel_misc:home" to the config)
65 | # nickel_misc - other stuff which isn't significant enough for its own category
66 | # nickel_open - opens a view
67 | # nickel_wifi - controls wifi (note: it doesn't wait for it to connect or disconnect, neither does it check for success)
68 | # nickel_bluetooth - controls bluetooth
69 | # nickel_orientation - controls screen orientation
70 | # (devices without an orientation sensor may need to use the kobopatch patch "Allow rotation on all devices" or set [DeveloperSettings] ForceAllowLandscape=true)
71 | # (devices with an orientation sensor don't need to do anything, but can set the config setting to make this work on all views)
72 | # (this will override the rotation icon/popup until it is set to something different or the device is rebooted)
73 | # power - gracefully controls the power state
74 | # skip - skips a number of actions after the current one (mainly useful for more complex conditional chains) (this action will not update the success/failure flag)
75 | # the argument passed to the action:
76 | # cmd_spawn - the command line to pass to /bin/sh -c (started in /)
77 | # It can be prefixed with "quiet:" to prevent the toast with the process PID from being displayed.
78 | # cmd_output - the timeout in milliseconds (0 < t < 10000), a colon, then the command line to pass to /bin/sh -c (started in /)
79 | # It can be prefixed with "quiet:" to prevent the message box with the output from being displayed (i.e. you'd use this where you'd normally use >/dev/null 2>&1).
80 | # dbg_syslog - the text to write
81 | # dbg_error - the error message
82 | # dbg_msg - the message
83 | # dbg_toast - the message
84 | # kfmon - the filename of the KFMon watched item to launch.
85 | # This is actually the basename of the watch's filename as specified in its KFMon config (i.e., the png).
86 | # You can also check the output of the 'list' command via the kfmon-ipc tool.
87 | # nickel_setting - :
88 | # action is one of:
89 | # toggle - toggles between true/false
90 | # enable - sets to true
91 | # disable - sets to false
92 | # setting is one of:
93 | # invert - FeatureSettings.InvertScreen (this requires a reboot to apply on 4.28.18220+, and may not work on newer devices)
94 | # dark_mode - ReadingSettings.DarkMode
95 | # lockscreen - PowerSettings.UnlockEnabled (4.12.12111+)
96 | # screenshots - FeatureSettings.Screenshots
97 | # force_wifi - DeveloperSettings.ForceWifiOn (note: the setting doesn't apply until you toggle WiFi)
98 | # auto_usb_gadget - Automatically enable USB mass storage on connection
99 | # nickel_extras - the mimetype of the plugin, or one of:
100 | # unblock_it
101 | # sketch_pad
102 | # solitaire
103 | # sudoku
104 | # word_scramble
105 | # nickel_browser - one of:
106 | # - opens the web browser to the default homepage (note that the line should end with a colon even though the argument is blank)
107 | # - opens the web browser to the specified URL
108 | # - opens the web browser to the specified URL and injects the specified CSS (which can contain spaces and colons) into all pages
109 | # modal - opens the web browser to the default homepage as a pop-up window
110 | # modal: - see above
111 | # modal: - see above
112 | # nickel_misc - one of:
113 | # home - goes to the home screen
114 | # force_usb_connection - forces a usb connection dialog to be shown
115 | # rescan_books - forces nickel to rescan books (4.13.12638+)
116 | # rescan_books_full - forces a full usb connect/disconnect cycle (4.13.12638+)
117 | # nickel_open - one of:
118 | # discover:storefront - Kobo Store
119 | # discover:wishlist - Wishlist
120 | # library:library - My Books (with last tab and filter)
121 | # library:all - Books
122 | # library:authors - Authors
123 | # library:series - Series (4.20.14601+)
124 | # library:shelves - Collections
125 | # library:pocket - Articles
126 | # library:dropbox - Dropbox (4.18.13737+)
127 | # reading_life:reading_life - Activity (with last tab)
128 | # reading_life:stats - Activity
129 | # reading_life:awards - Awards
130 | # reading_life:words - My Words
131 | # store:overdrive - OverDrive (4.10.11655+) (note: if you don't have an active OverDrive account, it will give you a "Network Error")
132 | # store:search - Search
133 | # nickel_wifi - one of:
134 | # autoconnect - attempts to enable and connect to wifi (similar to what happens when you open a link)
135 | # autoconnect_silent - attempts to connect to wifi in the background (does nothing if wifi is disabled, the battery is low, is already connected, or there aren't any known networks in range) (no errors are shown) (similar to what happens when you turn on the Kobo)
136 | # enable - enables WiFi (but doesn't necessarily connect to it)
137 | # disable - disables WiFi
138 | # toggle - toggles WiFi (but doesn't necessarily connect to it)
139 | # nickel_orientation - one of:
140 | # portrait (4.13.12638+)
141 | # landscape (4.13.12638+)
142 | # inverted_portrait (4.13.12638+)
143 | # inverted_landscape (4.13.12638+)
144 | # invert (4.13.12638+) - Toggles between inverted/non-inverted (preserves side) (this will not work if used in a chain with swap)
145 | # swap (4.13.12638+) - Toggles between portrait/landscape (preserves inversion) (this will not work if used in a chain with invert)
146 | # power - one of:
147 | # shutdown (4.13.12638+)
148 | # reboot (4.13.12638+)
149 | # sleep (4.13.12638+)
150 | # skip - the number of actions to skip, or -1 to skip all remaining ones (i.e. end the chain)
151 | # nickel_bluetooth - one of:
152 | # enable (4.34.20097+)
153 | # disable (4.34.20097+)
154 | # toggle (4.34.20097+)
155 | # check (4.34.20097+)
156 | # scan (4.34.20097+)
157 | #
158 | # chain_success::
159 | # chain_failure::
160 | # chain_always::
161 | # Adds an action to the chain that began with the preceding menu_item.
162 | # Actions are performed in the order they are written.
163 | # Each chain entry MUST follow the menu_item it is attached to. Another
164 | # menu_item marks the start of the next chain.
165 | # By default, each action only executes if the previous one was successful.
166 | # If chain_failure is used, the action is only executed if the last one
167 | # failed. If chain_always is used, the action is executed no matter what.
168 | # Any error message is only displayed if the action is the last one which
169 | # was executed in the chain (not necessarily the last one in the config
170 | # file).
171 | #
172 | # generator::
173 | # generator:::
174 | # Generates menu items dynamically during startup.
175 | #
176 | # the menu to add the items to, same as for menu_item.
177 | # the generator to use to generate the options, one of:
178 | # _test - generates numbered items, for testing only
179 | # kfmon - adds items from kfmon
180 | # the argument passed to the generator (if needed):
181 | # _test - the number of items to generate (0-10)
182 | # kfmon - one or none of:
183 | # gui - only enumerate non-hidden active KFMon watches (this is the default)
184 | # all - enumerate all active KFMon watches
185 | #
186 | # experimental::
187 | # Sets an experimental option. These are not guaranteed to be stable or be
188 | # compatible across NickelMenu or firmware versions, and may stop working at
189 | # any time. In addition, these options may not take effect without a reboot.
190 | # Unknown options are ignored. If there are multiple declarations of an
191 | # option, only the first one takes effect.
192 | #
193 | # the option name:
194 | # menu_main_15505_ - controls the added NickelMenu button
195 | # menu_main_15505__ - controls the main menu button at position n (indexed from 0)
196 | # note that there may be already-hidden buttons in the list:
197 | # on a Kobo Libra Colour running 4.41.23145, the button list is:
198 | # 0 - Home
199 | # 1 - My Books
200 | # 2 - Activity [hidden by default]
201 | # 3 - My Notebooks
202 | # 4 - Discover
203 | # 5 - More
204 | #
205 | # one of:
206 | # enabled - controls enablement of the NickelMenu button on 4.23.15505+
207 | # label - sets the label used for the NickelMenu button on 4.23.15505+
208 | # icon - sets the icon used for the NickelMenu button on 4.23.15505+
209 | # icon_active - sets the active icon used for the NickelMenu button on 4.23.15505+
210 | #
211 | # the option value:
212 | # (..)_enabled -> 0 or 1
213 | # (..)_label -> the label to use instead of the default value
214 | # (..)_icon -> the path passed to QPixmap
215 | # (..)_icon_active -> the path passed to QPixmap
216 | #
217 | # For example, you might have a configuration file named "mystuff" like:
218 | #
219 | # menu_item :main :Show an Error :dbg_error :This is an error message!
220 | # menu_item :main :Do Nothing :cmd_spawn :sleep 60
221 | # menu_item :main :Dump Syslog :cmd_spawn :logread > /mnt/onboard/.adds/syslog.log
222 | # menu_item :main :Kernel Version :cmd_output :500:uname -a
223 | # menu_item :main :Sketch Pad :nickel_extras :sketch_pad
224 | # menu_item :main :Plato :kfmon :plato.png
225 | # generator :main :kfmon
226 | # menu_item :reader :Invert Screen :nickel_setting :toggle :invert
227 | # menu_item :reader :Invert Orientation :nickel_orientation :invert
228 | # menu_item :main :IP Address :cmd_output :500:/sbin/ifconfig | /usr/bin/awk '/inet addr/{print substr($2,6)}'
229 | # menu_item :main :Telnet :cmd_spawn :quiet:/bin/mount -t devpts | /bin/grep -q /dev/pts || { /bin/mkdir -p /dev/pts && /bin/mount -t devpts devpts /dev/pts; }
230 | # chain_success :cmd_spawn :quiet:/usr/bin/pkill -f "^/usr/bin/tcpsvd -E 0.0.0.0 1023" || true && exec /usr/bin/tcpsvd -E 0.0.0.0 1023 /usr/sbin/telnetd -i -l /bin/login
231 | # chain_success :dbg_toast :Started Telnet server on port 1023.
232 | # menu_item :main :FTP :cmd_spawn :quiet:/usr/bin/pkill -f "^/usr/bin/tcpsvd -E 0.0.0.0 1021" || true && exec /usr/bin/tcpsvd -E 0.0.0.0 1021 /usr/sbin/ftpd -w -t 30 /mnt/onboard
233 | # chain_success :dbg_toast :Started FTP server for KOBOeReader partition on port 1021.
234 | # menu_item :main :Telnet (toggle) :cmd_output :500:quiet :/usr/bin/pkill -f "^/usr/bin/tcpsvd -E 0.0.0.0 2023"
235 | # chain_success:skip:5
236 | # chain_failure :cmd_spawn :quiet :/bin/mount -t devpts | /bin/grep -q /dev/pts || { /bin/mkdir -p /dev/pts && /bin/mount -t devpts devpts /dev/pts; }
237 | # chain_success :cmd_spawn :quiet :exec /usr/bin/tcpsvd -E 0.0.0.0 2023 /usr/sbin/telnetd -i -l /bin/login
238 | # chain_success :dbg_toast :Started Telnet server on port 2023
239 | # chain_failure :dbg_toast :Error starting Telnet server on port 2023
240 | # chain_always:skip:-1
241 | # chain_success :dbg_toast :Stopped Telnet server on port 2023
242 | # menu_item :library :Import books :nickel_misc :rescan_books_full
243 | # menu_item :browser :Invert Screen :nickel_setting :toggle :invert
244 | # menu_item :browser :Open Pop-Up :nickel_browser :modal
245 | #
246 | # And here are some examples of selection menu substitutions:
247 | #
248 | # menu_item :selection :Run command :cmd_output :500:echo '{1||$}'
249 | # menu_item :selection :Complex example :cmd_output :500:echo '{1|aS|"$}'
250 | # menu_item :selection_search :Search DuckDuckGo :nickel_browser :modal:https://duckduckgo.com/?q={1|S|%}
251 | # menu_item :selection_search :Search TLFi :nickel_browser :modal:https://www.cnrtl.fr/definition/{1|S|%} #menubox,#header,#nav,#footer,td>.box.bottombox{display:none} #wrap{max-width:100%;width:auto;border-width:0} .tab_box a{padding:8px 6px !important;margin-right:0 !important;border:none !important;background:0 0 !important} .tab_box{padding-bottom:8px} a[href*="/proxemie/"],a[href*="/aide/"]{display:none}
252 | #
253 | # If there is an error in the configuration, an item which displays it will be
254 | # added to the main menu. If an internal error occurs, it is written to syslog,
255 | # which can be viewed over telnet or SSH (the username is root) with the command
256 | # logread.
257 | #
258 | # To uninstall NickelMenu, create a file in this directory named "uninstall",
259 | # or manually uninstall it by deleting libnm.so. You can also uninstall it by
260 | # triggering the failsafe mechanism by turning your Kobo off within 20 seconds
261 | # of turning it on.
262 | #
263 | # To use NickelMenu to launch other software, you can usually use cmd_spawn with
264 | # the normal launch script. For example, KOReader and Plato will work as-is (or
265 | # with NiLuJe's one-click install packages which include NM). For specific
266 | # instructions, you should see their documentation (most newer software has
267 | # information about NickelMenu). If using cmd_spawn directly doesn't work, you
268 | # can also try using KFMon and the kfmon action/generator.
269 | #
270 | # - NiLuJe's KOReader/Plato packages: https://www.mobileread.com/forums/showthread.php?t=314220
271 | # - KOReader: https://github.com/koreader/koreader/wiki/Installation-on-Kobo-devices
272 | # - Plato: https://github.com/baskerville/plato/blob/master/doc/GUIDE.md
273 | # - Vlasovsoft: https://wiki.vlasovsoft.net/doku.php?id=en:installation
274 | #
275 | # Note: If you are embedding a NickelMenu config in your own mod, please name it
276 | # something unique to avoid overwriting someone's existing configuration. In
277 | # addition, please do not include NickelMenu itself in it unless you are
278 | # planning to update it as new releases are made.
279 | #
280 | # Note: Do not edit this file directly, as it will be overwritten when updating
281 | # NickelMenu.
282 |
--------------------------------------------------------------------------------
/src/action.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE // vasprintf
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #include "action.h"
8 |
9 | nm_action_result_t *nm_action_result_silent() {
10 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t));
11 | res->type = NM_ACTION_RESULT_TYPE_SILENT;
12 | return res;
13 | }
14 |
15 | #define _nm_action_result_fmt(_fn, _typ) \
16 | nm_action_result_t *nm_action_result_##_fn(const char *fmt, ...) { \
17 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t)); \
18 | res->type = _typ; \
19 | va_list v; \
20 | va_start(v, fmt); \
21 | if (vasprintf(&res->msg, fmt, v) == -1) \
22 | res->msg = strdup("error"); \
23 | va_end(v); \
24 | return res; \
25 | }
26 |
27 | _nm_action_result_fmt(msg, NM_ACTION_RESULT_TYPE_MSG);
28 | _nm_action_result_fmt(toast, NM_ACTION_RESULT_TYPE_TOAST);
29 |
30 | void nm_action_result_free(nm_action_result_t *res) {
31 | if (!res)
32 | return;
33 | if (res->msg)
34 | free(res->msg);
35 | free(res);
36 | }
37 |
--------------------------------------------------------------------------------
/src/action.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_ACTION_H
2 | #define NM_ACTION_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | typedef enum {
8 | NM_ACTION_RESULT_TYPE_SILENT = 0,
9 | NM_ACTION_RESULT_TYPE_MSG = 1,
10 | NM_ACTION_RESULT_TYPE_TOAST = 2,
11 | NM_ACTION_RESULT_TYPE_SKIP = 3, // for use by skip only
12 | } nm_action_result_type_t;
13 |
14 | typedef struct {
15 | nm_action_result_type_t type;
16 | char *msg;
17 | int skip; // for use by skip only
18 | } nm_action_result_t;
19 |
20 | // nm_action_fn_t represents an action. On success, a nm_action_result_t is
21 | // returned and needs to be freed with nm_action_result_free. Otherwise, NULL is
22 | // returned and nm_err is set.
23 | typedef nm_action_result_t *(*nm_action_fn_t)(const char *arg);
24 |
25 | nm_action_result_t *nm_action_result_silent();
26 | nm_action_result_t *nm_action_result_msg(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
27 | nm_action_result_t *nm_action_result_toast(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
28 | void nm_action_result_free(nm_action_result_t *res);
29 |
30 | #define NM_ACTION(name) nm_action_##name
31 |
32 | #ifdef __cplusplus
33 | #define NM_ACTION_(name) extern "C" nm_action_result_t *NM_ACTION(name)(const char *arg)
34 | #else
35 | #define NM_ACTION_(name) nm_action_result_t *NM_ACTION(name)(const char *arg)
36 | #endif
37 |
38 | #define NM_ACTIONS \
39 | X(cmd_spawn) \
40 | X(cmd_output) \
41 | X(dbg_syslog) \
42 | X(dbg_error) \
43 | X(dbg_msg) \
44 | X(dbg_toast) \
45 | X(kfmon) \
46 | X(kfmon_id) \
47 | X(nickel_setting) \
48 | X(nickel_extras) \
49 | X(nickel_browser) \
50 | X(nickel_misc) \
51 | X(nickel_open) \
52 | X(nickel_wifi) \
53 | X(nickel_bluetooth) \
54 | X(nickel_orientation) \
55 | X(power) \
56 | X(skip)
57 |
58 | #define X(name) NM_ACTION_(name);
59 | NM_ACTIONS
60 | #undef X
61 |
62 | #ifdef __cplusplus
63 | }
64 | #endif
65 | #endif
66 |
--------------------------------------------------------------------------------
/src/action_c.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "action.h"
4 | #include "kfmon.h"
5 | #include "util.h"
6 |
7 | NM_ACTION_(dbg_syslog) {
8 | NM_LOG("dbgsyslog: %s", arg);
9 | return nm_action_result_silent();
10 | }
11 |
12 | NM_ACTION_(dbg_error) {
13 | NM_ERR_SET("%s", arg);
14 | return NULL;
15 | }
16 |
17 | NM_ACTION_(dbg_msg) {
18 | return nm_action_result_msg("%s", arg);
19 | }
20 |
21 | NM_ACTION_(dbg_toast) {
22 | return nm_action_result_toast("%s", arg);
23 | }
24 |
25 | NM_ACTION_(skip) {
26 | char *tmp;
27 | long n = strtol(arg, &tmp, 10);
28 | NM_CHECK(NULL, *arg && !*tmp && n != 0 && n >= -1 && n < INT_MAX, "invalid count '%s': must be a nonzero integer or -1", arg);
29 |
30 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t));
31 | res->type = NM_ACTION_RESULT_TYPE_SKIP;
32 | res->skip = (int)(n);
33 | return res;
34 | }
35 |
36 | NM_ACTION_(kfmon_id) {
37 | // Start by watch ID (simpler, but IDs may not be stable across a single power cycle, given severe KFMon config shuffling)
38 | int status = nm_kfmon_simple_request("start", arg);
39 | return nm_kfmon_return_handler(status);
40 | }
41 |
42 | NM_ACTION_(kfmon) {
43 | // Trigger a watch, given its trigger basename. Stable runtime lookup done by KFMon.
44 | int status = nm_kfmon_simple_request("trigger", arg);
45 |
46 | // Fixup INVALID_ID to INVALID_NAME for slightly clearer feedback (see e8b2588 for details).
47 | if (status == KFMON_IPC_ERR_INVALID_ID)
48 | status = KFMON_IPC_ERR_INVALID_NAME;
49 |
50 | return nm_kfmon_return_handler(status);
51 | }
52 |
--------------------------------------------------------------------------------
/src/config.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE // asprintf
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include "action.h"
13 | #include "config.h"
14 | #include "generator.h"
15 | #include "nickelmenu.h"
16 | #include "util.h"
17 |
18 | struct nm_config_file_t {
19 | char *path;
20 | struct timespec mtime;
21 | nm_config_file_t *next;
22 | };
23 |
24 | // nm_config_files_filter skips special files, including:
25 | // - dotfiles
26 | // - vim: .*~, .*.s?? (unix only, usually swp or swo), *.swp, *.swo
27 | // - gedit: .*~
28 | // - emacs: .*~, #*#
29 | // - kate: .*.kate-swp
30 | // - macOS: .DS_Store*, .Spotlight-V*, ._*
31 | // - Windows: [Tt]humbs.db, desktop.ini
32 | static int nm_config_files_filter(const struct dirent *de) {
33 | const char *bn = de->d_name;
34 | char *ex = strrchr(bn, '.');
35 | ex = (ex && ex != bn && bn[0] != '.') ? ex : NULL;
36 | char lc = bn[strlen(bn)-1];
37 | if ((bn[0] == '.') ||
38 | (lc == '~') ||
39 | (bn[0] == '#' && lc == '#') ||
40 | (ex && (!strcmp(ex, ".swo") || !strcmp(ex, ".swp"))) ||
41 | (!strcmp(&bn[1], "humbs.db") && tolower(bn[0]) == 't') ||
42 | (!strcmp(bn, "desktop.ini"))) {
43 | NM_LOG("config: skipping %s/%s because it's a special file", NM_CONFIG_DIR, de->d_name);
44 | return 0;
45 | }
46 | return 1;
47 | }
48 |
49 | nm_config_file_t *nm_config_files() {
50 | nm_config_file_t *cfs = NULL, *cfc = NULL;
51 |
52 | struct dirent **nl;
53 | int n = scandir(NM_CONFIG_DIR, &nl, nm_config_files_filter, alphasort);
54 | NM_CHECK(NULL, n != -1, "could not scan config dir: %m");
55 |
56 | for (int i = 0; i < n; i++) {
57 | struct dirent *de = nl[i];
58 |
59 | char *fn;
60 | if (asprintf(&fn, "%s/%s", NM_CONFIG_DIR, de->d_name) == -1)
61 | fn = NULL;
62 |
63 | struct stat statbuf;
64 | if (!fn || stat(fn, &statbuf)) {
65 | while (cfs) {
66 | nm_config_file_t *tmp = cfs->next;
67 | free(cfs);
68 | cfs = tmp;
69 | }
70 | if (!fn)
71 | NM_ERR_RET(NULL, "could not build full path for config file");
72 | free(fn);
73 | NM_ERR_RET(NULL, "could not stat %s/%s", NM_CONFIG_DIR, de->d_name);
74 | }
75 |
76 | // skip it if it isn't a file
77 | if (de->d_type != DT_REG && !S_ISREG(statbuf.st_mode)) {
78 | NM_LOG("config: skipping %s because not a regular file", fn);
79 | free(fn);
80 | continue;
81 | }
82 |
83 | if (cfc) {
84 | cfc->next = calloc(1, sizeof(nm_config_file_t));
85 | cfc = cfc->next;
86 | } else {
87 | cfs = calloc(1, sizeof(nm_config_file_t));
88 | cfc = cfs;
89 | }
90 |
91 | cfc->path = fn;
92 | cfc->mtime = statbuf.st_mtim;
93 |
94 | free(de);
95 | }
96 |
97 | free(nl);
98 | nm_err_set(NULL);
99 | return cfs;
100 | }
101 |
102 | int nm_config_files_update(nm_config_file_t **files) {
103 | NM_CHECK(false, files, "files pointer must not be null");
104 |
105 | nm_config_file_t *nfiles = nm_config_files();
106 | if (nm_err_peek())
107 | return -1; // the error is passed on
108 |
109 | if (!*files) {
110 | *files = nfiles;
111 | return 0;
112 | }
113 |
114 | bool ch = false;
115 | nm_config_file_t *op = *files;
116 | nm_config_file_t *np = nfiles;
117 |
118 | while (op && np) {
119 | if (strcmp(op->path, np->path) || op->mtime.tv_sec != np->mtime.tv_sec || op->mtime.tv_nsec != np->mtime.tv_nsec) {
120 | ch = true;
121 | break;
122 | }
123 | op = op->next;
124 | np = np->next;
125 | }
126 |
127 | if (ch || op || np) {
128 | nm_config_files_free(*files);
129 | *files = nfiles;
130 | return 0;
131 | } else {
132 | nm_config_files_free(nfiles);
133 | return 1;
134 | }
135 | }
136 |
137 | void nm_config_files_free(nm_config_file_t *files) {
138 | while (files) {
139 | nm_config_file_t *tmp = files->next;
140 | free(files);
141 | files = tmp;
142 | }
143 | }
144 |
145 | typedef enum {
146 | NM_CONFIG_TYPE_MENU_ITEM = 1,
147 | NM_CONFIG_TYPE_GENERATOR = 2,
148 | NM_CONFIG_TYPE_EXPERIMENTAL = 3,
149 | } nm_config_type_t;
150 |
151 | typedef struct {
152 | char *key;
153 | char *val;
154 | } nm_config_experimental_t;
155 |
156 | struct nm_config_t {
157 | nm_config_type_t type;
158 | bool generated;
159 | union {
160 | nm_menu_item_t *menu_item;
161 | nm_generator_t *generator;
162 | nm_config_experimental_t *experimental;
163 | } value;
164 | nm_config_t *next;
165 | };
166 |
167 | // nm_config_parse__state_t contains the current state of the config parser. It
168 | // should be initialized to zero. The nm_config_parse__append__* functions will
169 | // deep-copy the item to append (to malloc'd memory). Each call will always
170 | // leave the state consistent, even on error (i.e. it will always be safe to
171 | // nm_config_free cfg_s).
172 | typedef struct nm_config_parse__state_t {
173 | nm_config_t *cfg_s; // config (first)
174 | nm_config_t *cfg_c; // config (current)
175 |
176 | nm_menu_item_t *cfg_it_c; // menu item (current)
177 | nm_menu_action_t *cfg_it_act_s; // menu action (first)
178 | nm_menu_action_t *cfg_it_act_c; // menu action (current)
179 |
180 | nm_generator_t *cfg_gn_c; // generator (current)
181 |
182 | nm_config_experimental_t *cfg_ex_c; // experimental (current)
183 | } nm_config_parse__state_t;
184 |
185 | typedef enum nm_config_parse__append__ret_t {
186 | NM_CONFIG_PARSE__APPEND__RET_OK = 0,
187 | NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR = 1,
188 | NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM = 2,
189 | } nm_config_parse__append__ret_t;
190 |
191 | static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it); // note: action pointer will be ignored (add it with append_action)
192 | static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act); // note: next pointer will be ignored (add another one by calling this again)
193 | static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn);
194 | static nm_config_parse__append__ret_t nm_config_parse__append_experimental(nm_config_parse__state_t *restrict state, nm_config_experimental_t *const restrict ex);
195 |
196 | static const char* nm_config_parse__strerror(nm_config_parse__append__ret_t ret) {
197 | switch (ret) {
198 | case NM_CONFIG_PARSE__APPEND__RET_OK:
199 | return NULL;
200 | case NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR:
201 | return "error allocating memory";
202 | case NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM:
203 | return "unexpected chain, must be directly after a menu item or another chain";
204 | default:
205 | return "unknown error";
206 | }
207 | }
208 |
209 | // note: line must point to the part after the config line type, and will be
210 | // modified. if the config line type doesn't match, everything will be
211 | // left as-is and false will be returned with nm_err cleared. if an error
212 | // occurs, nm_err will be set and true will be returned (since stuff
213 | // may be modified). otherwise, true is returned with nm_err cleared (the
214 | // parsed output will have strings pointing directly into the line).
215 |
216 | static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out);
217 | static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out);
218 | static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out);
219 | static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out);
220 | static bool nm_config_parse__line_experimental(const char *type, char **line, nm_config_experimental_t *ex_out);
221 |
222 | nm_config_t *nm_config_parse(nm_config_file_t *files) {
223 | const char *err = NULL;
224 |
225 | FILE *cfgfile = NULL;
226 | char *line = NULL;
227 | size_t line_bufsz = 0;
228 | int line_n;
229 | ssize_t line_sz;
230 |
231 | nm_config_parse__append__ret_t ret;
232 | nm_config_parse__state_t state = {0};
233 |
234 | nm_menu_item_t tmp_it;
235 | nm_menu_action_t tmp_act;
236 | nm_generator_t tmp_gn;
237 | nm_config_experimental_t tmp_ex;
238 |
239 | #define RETERR(fmt, ...) do { \
240 | NM_ERR_SET(fmt, ##__VA_ARGS__); \
241 | if (cfgfile) \
242 | fclose(cfgfile); \
243 | free(line); \
244 | nm_config_free(state.cfg_c); \
245 | return NULL; \
246 | } while (0)
247 |
248 | for (nm_config_file_t *cf = files; cf; cf = cf->next) {
249 | NM_LOG("config: reading config file %s", cf->path);
250 |
251 | line_n = 0;
252 | cfgfile = fopen(cf->path, "r");
253 |
254 | if (!cfgfile)
255 | RETERR("could not open file: %m");
256 |
257 | while ((line_sz = getline(&line, &line_bufsz, cfgfile)) != -1) {
258 | line_n++;
259 |
260 | char *cur = strtrim(line);
261 | if (!*cur || *cur == '#')
262 | continue; // empty line or comment
263 |
264 | char *s_typ = strtrim(strsep(&cur, ":"));
265 |
266 | if (nm_config_parse__line_item(s_typ, &cur, &tmp_it, &tmp_act)) {
267 | if ((err = nm_err()))
268 | RETERR("file %s: line %d: parse menu_item: %s",
269 | cf->path,
270 | line_n,
271 | err);
272 | if ((ret = nm_config_parse__append_item(&state, &tmp_it)))
273 | RETERR("file %s: line %d: error appending item to config: %s",
274 | cf->path,
275 | line_n,
276 | nm_config_parse__strerror(ret));
277 | if ((ret = nm_config_parse__append_action(&state, &tmp_act)))
278 | RETERR("file %s: line %d: error appending action to config: %s",
279 | cf->path,
280 | line_n,
281 | nm_config_parse__strerror(ret));
282 | continue;
283 | }
284 |
285 | if (nm_config_parse__line_chain(s_typ, &cur, &tmp_act)) {
286 | if ((err = nm_err()))
287 | RETERR("file %s: line %d: parse chain: %s",
288 | cf->path,
289 | line_n,
290 | err);
291 | if ((ret = nm_config_parse__append_action(&state, &tmp_act)))
292 | RETERR("file %s: line %d: error appending action to config: %s",
293 | cf->path,
294 | line_n,
295 | nm_config_parse__strerror(ret));
296 | continue;
297 | }
298 |
299 | if (nm_config_parse__line_generator(s_typ, &cur, &tmp_gn)) {
300 | if ((err = nm_err()))
301 | RETERR("file %s: line %d: parse generator: %s",
302 | cf->path,
303 | line_n,
304 | err);
305 | if ((ret = nm_config_parse__append_generator(&state, &tmp_gn)))
306 | RETERR("file %s: line %d: error appending generator to config: %s",
307 | cf->path,
308 | line_n,
309 | nm_config_parse__strerror(ret));
310 | continue;
311 | }
312 |
313 | if (nm_config_parse__line_experimental(s_typ, &cur, &tmp_ex)) {
314 | if ((err = nm_err()))
315 | RETERR("file %s: line %d: parse experimental option: %s",
316 | cf->path,
317 | line_n,
318 | err);
319 | if ((ret = nm_config_parse__append_experimental(&state, &tmp_ex)))
320 | RETERR("file %s: line %d: error appending experimental option to config: %s",
321 | cf->path,
322 | line_n,
323 | nm_config_parse__strerror(ret));
324 | continue;
325 | }
326 |
327 | RETERR("file %s: line %d: field 1: unknown type '%s'", cf->path, line_n, s_typ);
328 | }
329 |
330 | // reset the current per-file state
331 | state.cfg_it_c = NULL;
332 | state.cfg_it_act_s = NULL;
333 | state.cfg_it_act_c = NULL;
334 | state.cfg_gn_c = NULL;
335 | state.cfg_ex_c = NULL;
336 |
337 | fclose(cfgfile);
338 | cfgfile = NULL;
339 | }
340 |
341 | if (!state.cfg_c) {
342 | if ((ret = nm_config_parse__append_item(&state, &(nm_menu_item_t){
343 | .loc = NM_MENU_LOCATION(main),
344 | .lbl = "NickelMenu",
345 | .action = NULL,
346 | }))) RETERR("error appending default item to empty config: %s", nm_config_parse__strerror(ret));
347 |
348 | if ((ret = nm_config_parse__append_action(&state, &(nm_menu_action_t){
349 | .act = NM_ACTION(dbg_toast),
350 | .on_failure = true,
351 | .on_success = true,
352 | .arg = "See " NM_CONFIG_DIR_DISP "/doc for instructions on how to customize this menu.",
353 | .next = NULL,
354 | }))) RETERR("error appending default action to empty config: %s", nm_config_parse__strerror(ret));
355 | }
356 |
357 | #define X(name) \
358 | size_t c_##name = 0;
359 | NM_MENU_LOCATIONS
360 | #undef X
361 |
362 | for (nm_config_t *cur = state.cfg_s; cur; cur = cur->next) {
363 | switch (cur->type) {
364 | case NM_CONFIG_TYPE_MENU_ITEM:
365 | NM_LOG("cfg(NM_CONFIG_TYPE_MENU_ITEM) : %d:%s",
366 | cur->value.menu_item->loc,
367 | cur->value.menu_item->lbl);
368 | for (nm_menu_action_t *cur_act = cur->value.menu_item->action; cur_act; cur_act = cur_act->next)
369 | NM_LOG("...cfg(NM_CONFIG_TYPE_MENU_ITEM) (%s%s%s) : %p:%s",
370 | cur_act->on_success
371 | ? "on_success"
372 | : "",
373 | (cur_act->on_success && cur_act->on_failure)
374 | ? ", "
375 | : "",
376 | cur_act->on_failure
377 | ? "on_failure"
378 | : "",
379 | cur_act->act,
380 | cur_act->arg);
381 | switch (cur->value.menu_item->loc) {
382 | case NM_MENU_LOCATION_NONE: break;
383 | #define X(name) \
384 | case NM_MENU_LOCATION(name): c_##name++; break;
385 | NM_MENU_LOCATIONS
386 | #undef X
387 | }
388 | break;
389 | case NM_CONFIG_TYPE_GENERATOR:
390 | NM_LOG("cfg(NM_CONFIG_TYPE_GENERATOR) : %d:%s(%p):%s",
391 | cur->value.generator->loc,
392 | cur->value.generator->desc,
393 | cur->value.generator->generate,
394 | cur->value.generator->arg);
395 | break;
396 | case NM_CONFIG_TYPE_EXPERIMENTAL:
397 | NM_LOG("cfg(NM_CONFIG_TYPE_EXPERIMENTAL) : %s:%s",
398 | cur->value.experimental->key,
399 | cur->value.experimental->val);
400 | break;
401 | }
402 | }
403 |
404 | #define X(name) \
405 | if (c_##name > NM_CONFIG_MAX_MENU_ITEMS_PER_MENU) \
406 | RETERR("too many menu items in " #name " menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU);
407 | NM_MENU_LOCATIONS
408 | #undef X
409 |
410 | return state.cfg_s;
411 | }
412 |
413 | static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out) {
414 | if (strcmp(type, "menu_item")) {
415 | nm_err_set(NULL);
416 | return false;
417 | }
418 |
419 | *it_out = (nm_menu_item_t){0};
420 |
421 | char *s_loc = strtrim(strsep(line, ":"));
422 | if (!s_loc) NM_ERR_RET(true, "field 2: expected location, got end of line");
423 | #define X(name) \
424 | else if (!strcmp(s_loc, #name)) it_out->loc = NM_MENU_LOCATION(name);
425 | NM_MENU_LOCATIONS
426 | #undef X
427 | else NM_ERR_RET(true, "field 2: unknown location '%s'", s_loc);
428 |
429 | char *p_lbl = strtrim(strsep(line, ":"));
430 | if (!p_lbl) NM_ERR_RET(true, "field 3: expected label, got end of line");
431 | it_out->lbl = p_lbl;
432 |
433 | return nm_config_parse__lineend_action(4, line, true, true, action_out);
434 | }
435 |
436 | static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out) {
437 | if (strncmp(type, "chain_", 5)) {
438 | nm_err_set(NULL);
439 | return false;
440 | }
441 |
442 | bool p_on_success, p_on_failure;
443 | if (!strcmp(type, "chain_success")) {
444 | p_on_success = true;
445 | p_on_failure = false;
446 | } else if (!strcmp(type, "chain_always")) {
447 | p_on_success = true;
448 | p_on_failure = true;
449 | } else if (!strcmp(type, "chain_failure")) {
450 | p_on_success = false;
451 | p_on_failure = true;
452 | } else {
453 | nm_err_set(NULL);
454 | return false;
455 | }
456 |
457 | return nm_config_parse__lineend_action(2, line, p_on_success, p_on_failure, act_out);
458 | }
459 |
460 | static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out) {
461 | if (strcmp(type, "generator")) {
462 | nm_err_set(NULL);
463 | return false;
464 | }
465 |
466 | *gn_out = (nm_generator_t){0};
467 |
468 | char *s_loc = strtrim(strsep(line, ":"));
469 | if (!s_loc) NM_ERR_RET(true, "field 2: expected location, got end of line");
470 | #define X(name) \
471 | else if (!strcmp(s_loc, #name)) gn_out->loc = NM_MENU_LOCATION(name);
472 | NM_MENU_LOCATIONS
473 | #undef X
474 | else NM_ERR_RET(true, "field 2: unknown location '%s'", s_loc);
475 |
476 | char *s_generate = strtrim(strsep(line, ":"));
477 | if (!s_generate) NM_ERR_RET(true, "field 3: expected generator, got end of line");
478 | #define X(name) \
479 | else if (!strcmp(s_generate, #name)) gn_out->generate = NM_GENERATOR(name);
480 | NM_GENERATORS
481 | #undef X
482 | else NM_ERR_RET(true, "field 3: unknown generator '%s'", s_generate);
483 |
484 | char *p_arg = strtrim(*line); // note: optional
485 | if (p_arg) gn_out->arg = p_arg;
486 |
487 | gn_out->desc = s_generate;
488 |
489 | nm_err_set(NULL);
490 | return true;
491 | }
492 |
493 | static bool nm_config_parse__line_experimental(const char *type, char **line, nm_config_experimental_t *ex_out) {
494 | if (strcmp(type, "experimental")) {
495 | nm_err_set(NULL);
496 | return false;
497 | }
498 |
499 | *ex_out = (nm_config_experimental_t){0};
500 |
501 | ex_out->key = strtrim(strsep(line, ":"));
502 | if (!ex_out->key)
503 | NM_ERR_RET(true, "field 2: expected key, got end of line");
504 |
505 | ex_out->val = strtrim(strsep(line, ":"));
506 | if (!ex_out->val)
507 | NM_ERR_RET(true, "field 2: expected val, got end of line");
508 |
509 | nm_err_set(NULL);
510 | return true;
511 | }
512 |
513 | static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out) {
514 | *act_out = (nm_menu_action_t){0};
515 |
516 | char *s_act = strtrim(strsep(line, ":"));
517 | if (!s_act) NM_ERR_RET(true, "field %d: expected action, got end of line", field);
518 | #define X(name) \
519 | else if (!strcmp(s_act, #name)) act_out->act = NM_ACTION(name);
520 | NM_ACTIONS
521 | #undef X
522 | else NM_ERR_RET(true, "field %d: unknown action '%s'", field, s_act);
523 |
524 | // type: menu_item - field 5: argument
525 | char *p_arg = strtrim(*line);
526 | if (!p_arg) NM_ERR_RET(true, "field %d: expected argument, got end of line\n", field+1);
527 | act_out->arg = p_arg;
528 |
529 | act_out->on_success = p_on_success;
530 | act_out->on_failure = p_on_failure;
531 |
532 | nm_err_set(NULL);
533 | return true;
534 | }
535 |
536 | static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it) {
537 | nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t));
538 | nm_menu_item_t *cfg_it_n = calloc(1, sizeof(nm_menu_item_t));
539 |
540 | if (!cfg_n || !cfg_it_n) {
541 | free(cfg_n);
542 | free(cfg_it_n);
543 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
544 | }
545 |
546 | *cfg_n = (nm_config_t){
547 | .type = NM_CONFIG_TYPE_MENU_ITEM,
548 | .generated = false,
549 | .value = { .menu_item = cfg_it_n },
550 | .next = NULL,
551 | };
552 |
553 | *cfg_it_n = (nm_menu_item_t){
554 | .loc = it->loc,
555 | .lbl = strdup(it->lbl ? it->lbl : ""),
556 | .action = NULL,
557 | };
558 |
559 | if (!cfg_it_n->lbl) {
560 | free(cfg_n);
561 | free(cfg_it_n);
562 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
563 | }
564 |
565 | if (state->cfg_c)
566 | state->cfg_c->next = cfg_n;
567 | else
568 | state->cfg_s = cfg_n;
569 |
570 | state->cfg_c = cfg_n;
571 | state->cfg_it_c = cfg_it_n;
572 |
573 | state->cfg_it_act_s = NULL;
574 | state->cfg_it_act_c = NULL;
575 | state->cfg_gn_c = NULL;
576 | state->cfg_ex_c = NULL;
577 |
578 | return NM_CONFIG_PARSE__APPEND__RET_OK;
579 | }
580 |
581 | static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act) {
582 | if (!state->cfg_c || !state->cfg_it_c || state->cfg_gn_c)
583 | return NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM;
584 |
585 | nm_menu_action_t *cfg_it_act_n = calloc(1, sizeof(nm_menu_action_t));
586 |
587 | if (!cfg_it_act_n)
588 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
589 |
590 | *cfg_it_act_n = (nm_menu_action_t){
591 | .act = act->act,
592 | .on_failure = act->on_failure,
593 | .on_success = act->on_success,
594 | .arg = strdup(act->arg ? act->arg : ""),
595 | .next = NULL,
596 | };
597 |
598 | if (!cfg_it_act_n->arg) {
599 | free(cfg_it_act_n);
600 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
601 | }
602 |
603 | if (!state->cfg_it_c->action)
604 | state->cfg_it_c->action = cfg_it_act_n;
605 |
606 | if (state->cfg_it_act_c)
607 | state->cfg_it_act_c->next = cfg_it_act_n;
608 | else
609 | state->cfg_it_act_s = cfg_it_act_n;
610 |
611 | state->cfg_it_act_c = cfg_it_act_n;
612 |
613 | return NM_CONFIG_PARSE__APPEND__RET_OK;
614 | }
615 |
616 | static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn) {
617 | nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t));
618 | nm_generator_t *cfg_gn_n = calloc(1, sizeof(nm_generator_t));
619 |
620 | if (!cfg_n || !cfg_gn_n) {
621 | free(cfg_n);
622 | free(cfg_gn_n);
623 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
624 | }
625 |
626 | *cfg_n = (nm_config_t){
627 | .type = NM_CONFIG_TYPE_GENERATOR,
628 | .generated = false,
629 | .value = { .generator = cfg_gn_n },
630 | .next = NULL,
631 | };
632 |
633 | *cfg_gn_n = (nm_generator_t){
634 | .desc = strdup(gn->desc ? gn->desc : ""),
635 | .loc = gn->loc,
636 | .arg = strdup(gn->arg ? gn->arg : ""),
637 | .generate = gn->generate,
638 | };
639 |
640 | if (!cfg_gn_n->desc || !cfg_gn_n->arg) {
641 | free(cfg_gn_n->desc);
642 | free(cfg_gn_n->arg);
643 | free(cfg_n);
644 | free(cfg_gn_n);
645 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
646 | }
647 |
648 | if (state->cfg_c)
649 | state->cfg_c->next = cfg_n;
650 | else
651 | state->cfg_s = cfg_n;
652 |
653 | state->cfg_c = cfg_n;
654 | state->cfg_gn_c = cfg_gn_n;
655 |
656 | state->cfg_it_c = NULL;
657 | state->cfg_it_act_s = NULL;
658 | state->cfg_it_act_c = NULL;
659 | state->cfg_ex_c = NULL;
660 |
661 | return NM_CONFIG_PARSE__APPEND__RET_OK;
662 | }
663 |
664 | static nm_config_parse__append__ret_t nm_config_parse__append_experimental(nm_config_parse__state_t *restrict state, nm_config_experimental_t *const restrict ex) {
665 | nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t));
666 | nm_config_experimental_t *cfg_ex_n = calloc(1, sizeof(nm_config_experimental_t));
667 |
668 | if (!cfg_n || !cfg_ex_n) {
669 | free(cfg_n);
670 | free(cfg_ex_n);
671 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
672 | }
673 |
674 | *cfg_n = (nm_config_t){
675 | .type = NM_CONFIG_TYPE_EXPERIMENTAL,
676 | .generated = false,
677 | .value = { .experimental = cfg_ex_n },
678 | .next = NULL,
679 | };
680 |
681 | *cfg_ex_n = (nm_config_experimental_t){
682 | .key = strdup(ex->key ? ex->key : ""),
683 | .val = strdup(ex->val ? ex->val : ""),
684 | };
685 |
686 | if (!cfg_ex_n->key || !cfg_ex_n->val) {
687 | free(cfg_ex_n->key);
688 | free(cfg_ex_n->val);
689 | free(cfg_n);
690 | free(cfg_ex_n);
691 | return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR;
692 | }
693 |
694 | if (state->cfg_c)
695 | state->cfg_c->next = cfg_n;
696 | else
697 | state->cfg_s = cfg_n;
698 |
699 | state->cfg_c = cfg_n;
700 | state->cfg_ex_c = cfg_ex_n;
701 |
702 | state->cfg_it_c = NULL;
703 | state->cfg_it_act_s = NULL;
704 | state->cfg_it_act_c = NULL;
705 | state->cfg_gn_c = NULL;
706 |
707 | return NM_CONFIG_PARSE__APPEND__RET_OK;
708 | }
709 |
710 | bool nm_config_generate(nm_config_t *cfg, bool force_update) {
711 | bool changed = false;
712 |
713 | NM_LOG("config: running generators");
714 | for (nm_config_t *cur = cfg; cur; cur = cur->next) {
715 | if (cur->type == NM_CONFIG_TYPE_GENERATOR) {
716 | NM_LOG("config: running generator %s:%s", cur->value.generator->desc, cur->value.generator->arg);
717 |
718 | if (force_update)
719 | cur->value.generator->time = (struct timespec){0, 0};
720 |
721 | size_t sz;
722 | nm_menu_item_t **items = nm_generator_do(cur->value.generator, &sz);
723 | if (!items) {
724 | NM_LOG("config: ... no new items generated");
725 | if (force_update)
726 | NM_LOG("config: ... possible bug: no items were generated even with force_update");
727 | continue;
728 | }
729 |
730 | NM_LOG("config: ... %zu items generated, removing previously generated items and replacing with new ones", sz);
731 |
732 | changed = true;
733 |
734 | // remove all generated items immediately after the generator
735 | for (nm_config_t *t_prev = cur, *t_cur = cur->next; t_cur && t_cur->generated; t_cur = t_cur->next) {
736 | t_prev->next = t_cur->next; // skip the generated item
737 | t_cur->next = NULL; // so we only free that item
738 | nm_config_free(t_cur);
739 | t_cur = t_prev; // continue with the new current item
740 | }
741 |
742 | // add the new ones
743 | for (ssize_t i = sz-1; i >= 0; i--) {
744 | nm_config_t *tmp = calloc(1, sizeof(nm_config_t));
745 | tmp->type = NM_CONFIG_TYPE_MENU_ITEM;
746 | tmp->value.menu_item = items[i];
747 | tmp->generated = true;
748 | tmp->next = cur->next;
749 | cur->next = tmp;
750 | }
751 | free(items);
752 | }
753 | }
754 |
755 | NM_LOG("config: generated items");
756 | for (nm_config_t *cur = cfg; cur; cur = cur->next) {
757 | if (cur->generated) {
758 | switch (cur->type) {
759 | case NM_CONFIG_TYPE_MENU_ITEM:
760 | NM_LOG("cfg(NM_CONFIG_TYPE_MENU_ITEM) : %d:%s",
761 | cur->value.menu_item->loc,
762 | cur->value.menu_item->lbl);
763 | for (nm_menu_action_t *cur_act = cur->value.menu_item->action; cur_act; cur_act = cur_act->next)
764 | NM_LOG("...cfg(NM_CONFIG_TYPE_MENU_ITEM) (%s%s%s) : %p:%s",
765 | cur_act->on_success ? "on_success" : "",
766 | (cur_act->on_success && cur_act->on_failure) ? ", " : "",
767 | cur_act->on_failure ? "on_failure" : "",
768 | cur_act->act,
769 | cur_act->arg);
770 | break;
771 | case NM_CONFIG_TYPE_GENERATOR:
772 | NM_LOG("cfg(NM_CONFIG_TYPE_GENERATOR) : %d:%s(%p):%s",
773 | cur->value.generator->loc,
774 | cur->value.generator->desc,
775 | cur->value.generator->generate,
776 | cur->value.generator->arg);
777 | break;
778 | case NM_CONFIG_TYPE_EXPERIMENTAL:
779 | NM_LOG("cfg(NM_CONFIG_TYPE_EXPERIMENTAL) : %s:%s",
780 | cur->value.experimental->key,
781 | cur->value.experimental->val);
782 | break;
783 | }
784 | }
785 | }
786 |
787 | return changed;
788 | }
789 |
790 | nm_menu_item_t **nm_config_get_menu(nm_config_t *cfg, size_t *n_out) {
791 | *n_out = 0;
792 | for (nm_config_t *cur = cfg; cur; cur = cur->next)
793 | if (cur->type == NM_CONFIG_TYPE_MENU_ITEM)
794 | (*n_out)++;
795 |
796 | nm_menu_item_t **it = calloc(*n_out, sizeof(nm_menu_item_t*));
797 | if (!it)
798 | return NULL;
799 |
800 | nm_menu_item_t **tmp = it;
801 | for (nm_config_t *cur = cfg; cur; cur = cur->next)
802 | if (cur->type == NM_CONFIG_TYPE_MENU_ITEM)
803 | *(tmp++) = cur->value.menu_item;
804 |
805 | return it;
806 | }
807 |
808 | const char *nm_config_experimental(nm_config_t *cfg, const char *key) {
809 | if (key)
810 | for (nm_config_t *cur = cfg; cur; cur = cur->next)
811 | if (cur->type == NM_CONFIG_TYPE_EXPERIMENTAL)
812 | if (!strcmp(cur->value.experimental->key, key))
813 | return cur->value.experimental->val;
814 | return NULL;
815 | }
816 |
817 | void nm_config_free(nm_config_t *cfg) {
818 | while (cfg) {
819 | nm_config_t *n = cfg->next;
820 |
821 | switch (cfg->type) {
822 | case NM_CONFIG_TYPE_MENU_ITEM:
823 | free(cfg->value.menu_item->lbl);
824 | if (cfg->value.menu_item->action) {
825 | nm_menu_action_t *cur = cfg->value.menu_item->action;
826 | nm_menu_action_t *tmp;
827 | while (cur) {
828 | tmp = cur;
829 | cur = cur->next;
830 | free(tmp->arg);
831 | free(tmp);
832 | }
833 | }
834 | free(cfg->value.menu_item);
835 | break;
836 | case NM_CONFIG_TYPE_GENERATOR:
837 | free(cfg->value.generator->arg);
838 | free(cfg->value.generator->desc);
839 | free(cfg->value.generator);
840 | break;
841 | case NM_CONFIG_TYPE_EXPERIMENTAL:
842 | free(cfg->value.experimental->key);
843 | free(cfg->value.experimental->val);
844 | free(cfg->value.experimental);
845 | break;
846 | }
847 | free(cfg);
848 |
849 | cfg = n;
850 | }
851 | }
852 |
853 | // note: not thread safe
854 | static nm_config_file_t *nm_global_menu_config_files = NULL; // updated in-place by nm_global_config_update
855 | static nm_config_t *nm_global_menu_config = NULL; // updated by nm_global_config_update, replaced by nm_global_config_replace, NULL on error
856 | static nm_menu_item_t **nm_global_menu_config_items = NULL; // updated by nm_global_config_replace to an error message or the items from nm_global_menu_config
857 | static size_t nm_global_menu_config_n = 0; // ^
858 | static int nm_global_menu_config_rev = -1; // incremented by nm_global_config_update whenever the config items change for any reason
859 |
860 | nm_menu_item_t **nm_global_config_items(size_t *n_out) {
861 | if (n_out)
862 | *n_out = nm_global_menu_config_n;
863 | return nm_global_menu_config_items;
864 | }
865 |
866 | const char *nm_global_config_experimental(const char *key) {
867 | return nm_config_experimental(nm_global_menu_config, key);
868 | }
869 |
870 | static void nm_global_config_replace(nm_config_t *cfg, const char *err) {
871 | if (nm_global_menu_config_n)
872 | nm_global_menu_config_n = 0;
873 |
874 | if (nm_global_menu_config_items) {
875 | free(nm_global_menu_config_items);
876 | nm_global_menu_config_items = NULL;
877 | }
878 |
879 | if (nm_global_menu_config) {
880 | nm_config_free(nm_global_menu_config);
881 | nm_global_menu_config = NULL;
882 | }
883 |
884 | if (err && cfg)
885 | nm_config_free(cfg);
886 |
887 | // this isn't strictly necessary, but we should always try to reparse it
888 | // every time just in case the error was temporary
889 | if (err && nm_global_menu_config_files) {
890 | nm_config_files_free(nm_global_menu_config_files);
891 | nm_global_menu_config_files = NULL;
892 | }
893 |
894 | if (err) {
895 | nm_global_menu_config_n = 1;
896 | nm_global_menu_config_items = calloc(nm_global_menu_config_n, sizeof(nm_menu_item_t*));
897 | nm_global_menu_config_items[0] = calloc(1, sizeof(nm_menu_item_t));
898 | nm_global_menu_config_items[0]->loc = NM_MENU_LOCATION(main);
899 | nm_global_menu_config_items[0]->lbl = strdup("Config Error");
900 | nm_global_menu_config_items[0]->action = calloc(1, sizeof(nm_menu_action_t));
901 | nm_global_menu_config_items[0]->action->arg = strdup(err);
902 | nm_global_menu_config_items[0]->action->act = NM_ACTION(dbg_msg);
903 | nm_global_menu_config_items[0]->action->on_failure = true;
904 | nm_global_menu_config_items[0]->action->on_success = true;
905 | return;
906 | }
907 |
908 | nm_global_menu_config = cfg;
909 | nm_global_menu_config_items = nm_config_get_menu(cfg, &nm_global_menu_config_n);
910 | if (!nm_global_menu_config_items)
911 | NM_LOG("could not allocate memory");
912 | }
913 |
914 | int nm_global_config_update() {
915 | NM_LOG("global: scanning for config files");
916 | int state = nm_config_files_update(&nm_global_menu_config_files);
917 | if (state == -1) {
918 | const char *err = nm_err();
919 | NM_LOG("... error: %s", err);
920 | NM_LOG("global: freeing old config and replacing with error item");
921 | nm_global_config_replace(NULL, err);
922 | nm_global_menu_config_rev++;
923 | NM_ERR_RET(nm_global_menu_config_rev, "scan for config files: %s", err);
924 | }
925 | NM_LOG("global:%s changes detected", state == 0 ? "" : " no");
926 |
927 | if (state == 0) {
928 | NM_LOG("global: parsing new config");
929 | nm_config_t *cfg = nm_config_parse(nm_global_menu_config_files);
930 | if (!cfg) {
931 | const char *err = nm_err();
932 | NM_LOG("... error: %s", err);
933 | NM_LOG("global: freeing old config and replacing with error item");
934 | nm_global_config_replace(NULL, err);
935 | nm_global_menu_config_rev++;
936 | NM_ERR_RET(nm_global_menu_config_rev, "parse config files: %s", err);
937 | }
938 |
939 | NM_LOG("global: config updated, freeing old config and replacing with new one");
940 | nm_global_config_replace(cfg, NULL);
941 | nm_global_menu_config_rev++;
942 | NM_LOG("global: done swapping config");
943 | }
944 |
945 | NM_LOG("global: running generators");
946 | bool g_updated = nm_config_generate(nm_global_menu_config, false);
947 | NM_LOG("global:%s generators updated", g_updated ? "" : " no");
948 |
949 | if (g_updated) {
950 | NM_LOG("global: generators updated, freeing old items and replacing with new ones");
951 |
952 | if (nm_global_menu_config_n)
953 | nm_global_menu_config_n = 0;
954 |
955 | if (nm_global_menu_config_items) {
956 | free(nm_global_menu_config_items);
957 | nm_global_menu_config_items = NULL;
958 | }
959 |
960 | nm_global_menu_config_items = nm_config_get_menu(nm_global_menu_config, &nm_global_menu_config_n);
961 | if (!nm_global_menu_config_items)
962 | NM_LOG("could not allocate memory");
963 |
964 | nm_global_menu_config_rev++;
965 | NM_LOG("done replacing items");
966 | }
967 |
968 | nm_err_set(NULL);
969 | return nm_global_menu_config_rev;
970 | }
971 |
--------------------------------------------------------------------------------
/src/config.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_CONFIG_H
2 | #define NM_CONFIG_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | #include
8 | #include
9 |
10 | #include "action.h"
11 | #include "nickelmenu.h"
12 |
13 | #if !(defined(NM_CONFIG_DIR) && defined(NM_CONFIG_DIR_DISP))
14 | #error "NM_CONFIG_DIR not set (it should be done by the Makefile)"
15 | #endif
16 |
17 | #ifndef NM_CONFIG_MAX_MENU_ITEMS_PER_MENU
18 | #define NM_CONFIG_MAX_MENU_ITEMS_PER_MENU 50
19 | #endif
20 |
21 | typedef struct nm_config_t nm_config_t;
22 |
23 | typedef struct nm_config_file_t nm_config_file_t;
24 |
25 | // nm_config_parse lists the configuration files in NM_CONFIG_DIR. If there are
26 | // errors reading the dir, NULL is returned and nm_err is set.
27 | nm_config_file_t *nm_config_files();
28 |
29 | // nm_config_files_update checks if the configuration files are up to date and
30 | // updates them. If the files are already up-to-date, 1 is returned. If the
31 | // files were updated, 0 is returned. If an error occurs, the pointer is left
32 | // untouched and -1 is returned with nm_err set. Warning: if the files have
33 | // changed, the pointer passed to files will become invalid (it gets replaced).
34 | // If *files is NULL, it is equivalent to doing `*files = nm_config_files()`.
35 | int nm_config_files_update(nm_config_file_t **files);
36 |
37 | // nm_config_files_free frees the list of configuration files.
38 | void nm_config_files_free(nm_config_file_t *files);
39 |
40 | // nm_config_parse parses the configuration files. If there are syntax errors,
41 | // file access errors, or invalid action names for menu_item, then NULL is
42 | // returned and nm_err is set. On success, the config is returned.
43 | nm_config_t *nm_config_parse(nm_config_file_t *files);
44 |
45 | // nm_config_generate runs all generators synchronously and sequentially. Any
46 | // previously generated items are automatically removed if updates are required.
47 | // If the config was modified, true is returned.
48 | bool nm_config_generate(nm_config_t *cfg, bool force_update);
49 |
50 | // nm_config_get_menu gets a malloc'd array of pointers to the menu items
51 | // defined in the config. These pointers will be valid until nm_config_free is
52 | // called.
53 | nm_menu_item_t **nm_config_get_menu(nm_config_t *cfg, size_t *n_out);
54 |
55 | // nm_config_experimental gets the first value of an arbitrary experimental
56 | // option. If it doesn't exist, NULL will be returned. The pointer will be valid
57 | // until nm_config_free is called.
58 | const char *nm_config_experimental(nm_config_t *cfg, const char *key);
59 |
60 | // nm_config_free frees all allocated memory.
61 | void nm_config_free(nm_config_t *cfg);
62 |
63 | // nm_global_config_update updates and regenerates the config if needed. If the
64 | // menu items changed (i.e. the old items aren't valid anymore), the revision
65 | // will be incremented and returned (even if there was an error). On error,
66 | // nm_err is set, and otherwise, it is cleared.
67 | int nm_global_config_update();
68 |
69 | // nm_global_config_items returns an array of pointers with the current menu
70 | // items (the pointer and the items it points to will remain valid until the
71 | // next time nm_global_config_update is called). The number of items is stored
72 | // in the variable pointed to by n_out. If an error ocurred during the last time
73 | // nm_global_config_update was called, it is returned as a "Config Error" menu
74 | // item. If nm_global_config_update has never been called successfully before,
75 | // NULL is returned and n_out is set to 0.
76 | nm_menu_item_t **nm_global_config_items(size_t *n_out);
77 |
78 | // nm_global_config_experimental gets the first value of an arbitrary
79 | // experimental option (the pointer will remain valid until the next time
80 | // nm_global_config_update is called). If it doesn't exist, NULL will be
81 | // returned.
82 | const char *nm_global_config_experimental(const char *key);
83 |
84 | #ifdef __cplusplus
85 | }
86 | #endif
87 | #endif
88 |
--------------------------------------------------------------------------------
/src/generator.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE // asprintf
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #include "action.h"
8 | #include "generator.h"
9 | #include "nickelmenu.h"
10 | #include "util.h"
11 |
12 | nm_menu_item_t **nm_generator_do(nm_generator_t *gen, size_t *sz_out) {
13 | NM_LOG("generator: running generator (%s) (%s) (%d) (%p)", gen->desc, gen->arg, gen->loc, gen->generate);
14 |
15 | struct timespec old = gen->time;
16 | size_t sz = (size_t)(-1); // this should always be set by generate upon success, but we'll initialize it just in case
17 | nm_menu_item_t **items = gen->generate(gen->arg, &gen->time, &sz);
18 |
19 | if (items && old.tv_sec == gen->time.tv_sec && old.tv_nsec == gen->time.tv_nsec)
20 | NM_LOG("generator: bug: new items were returned, but time wasn't changed");
21 |
22 | const char *err = nm_err();
23 |
24 | if (!old.tv_sec && !old.tv_nsec && !err && !items)
25 | NM_LOG("generator: warning: no existing items (time == 0), but no new items or error were returned");
26 |
27 | if (err) {
28 | if (items)
29 | NM_LOG("generator: bug: items should be null on error");
30 |
31 | NM_LOG("generator: generator error (%s) (%s), replacing with error item: %s", gen->desc, gen->arg, err);
32 | sz = 1;
33 | items = calloc(sz, sizeof(nm_menu_item_t*));
34 | items[0] = calloc(1, sizeof(nm_menu_item_t));
35 | // loc will be set below
36 | items[0]->lbl = strdup("Generator error");
37 | items[0]->action = calloc(1, sizeof(nm_menu_action_t));
38 | items[0]->action->act = NM_ACTION(dbg_msg);
39 | asprintf(&items[0]->action->arg, "%s: %s", gen->desc, err);
40 | items[0]->action->on_failure = true;
41 | items[0]->action->on_success = true;
42 | }
43 |
44 | if (!err && !items && (old.tv_sec != gen->time.tv_sec || old.tv_nsec != gen->time.tv_nsec))
45 | NM_LOG("generator: bug: the time should have been updated if new items were returned");
46 |
47 | if (items) {
48 | if (sz == (size_t)(-1))
49 | NM_LOG("generator: bug: size should have been set by generate, but wasn't");
50 | if (!sz)
51 | NM_LOG("generator: bug: items should be null when size is 0");
52 |
53 | for (size_t i = 0; i < sz; i++) {
54 | if (items[i]->loc)
55 | NM_LOG("generator: bug: generator should not set the menu item location, as it will be overridden");
56 |
57 | items[i]->loc = gen->loc;
58 | }
59 | }
60 |
61 | *sz_out = sz;
62 | return items;
63 | }
64 |
--------------------------------------------------------------------------------
/src/generator.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_GENERATOR_H
2 | #define NM_GENERATOR_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | #include
8 | #include
9 | #include "nickelmenu.h"
10 |
11 | // nm_generator_fn_t generates menu items. It must return a malloc'd array of
12 | // pointers to malloc'd nm_menu_item_t's, and write the number of items to
13 | // out_sz. The menu item locations must not be set. On error, nm_err must be
14 | // set, NULL must be returned, and sz_out is undefined. If no entries are
15 | // generated, NULL must be returned with sz_out set to 0. All strings should
16 | // also be malloc'd. On success, nm_err must be cleared.
17 | //
18 | // time_in_out will not be NULL, and contains zero or the last modification time
19 | // for the generator. If it is zero, the generator should generate the items as
20 | // usual and update the time. If it is nonzero, the generator should return NULL
21 | // without making changes if the time is up to date (the check should be as
22 | // quick as possible), and if not, it should update the items and update the
23 | // time to match. If the generator does not have a way of checking for updates
24 | // quickly, it should only update the item and set the time to a nonzero value
25 | // if the time is zero, and return NULL if the time is nonzero. Note that this
26 | // time doesn't have to account for different arguments or multiple instances,
27 | // as changes in those will always cause the time to be set to zero.
28 | typedef nm_menu_item_t **(*nm_generator_fn_t)(const char *arg, struct timespec *time_in_out, size_t *sz_out);
29 |
30 | typedef struct {
31 | char *desc; // only used for making the errors more meaningful (it is the title)
32 | char *arg;
33 | nm_menu_location_t loc;
34 | nm_generator_fn_t generate; // should be as quick as possible with a short timeout, as it will block startup
35 | struct timespec time;
36 | } nm_generator_t;
37 |
38 | // nm_generator_do runs a generator and returns the generated items, if any, or
39 | // an item which shows the error returned by the generator. If NULL is returned,
40 | // no items needed to be updated (set time to zero to force an update) (sz_out
41 | // is undefined).
42 | nm_menu_item_t **nm_generator_do(nm_generator_t *gen, size_t *sz_out);
43 |
44 | #define NM_GENERATOR(name) nm_generator_##name
45 |
46 | #ifdef __cplusplus
47 | #define NM_GENERATOR_(name) extern "C" nm_menu_item_t **NM_GENERATOR(name)(const char *arg, struct timespec *time_in_out, size_t *sz_out)
48 | #else
49 | #define NM_GENERATOR_(name) nm_menu_item_t **NM_GENERATOR(name)(const char *arg, struct timespec *time_in_out, size_t *sz_out)
50 | #endif
51 |
52 | #define NM_GENERATORS \
53 | X(_test) \
54 | X(_test_time) \
55 | X(kfmon)
56 |
57 | #define X(name) NM_GENERATOR_(name);
58 | NM_GENERATORS
59 | #undef X
60 |
61 | #ifdef __cplusplus
62 | }
63 | #endif
64 | #endif
65 |
--------------------------------------------------------------------------------
/src/generator_c.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE // asprintf
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include "generator.h"
12 | #include "kfmon.h"
13 | #include "nickelmenu.h"
14 | #include "util.h"
15 |
16 | NM_GENERATOR_(_test) {
17 | if (time_in_out->tv_sec || time_in_out->tv_nsec) {
18 | nm_err_set(NULL);
19 | return NULL; // updates not supported (or needed, for that matter)
20 | }
21 |
22 | char *tmp;
23 | long n = strtol(arg, &tmp, 10);
24 | NM_CHECK(NULL, *arg && !*tmp && n >= 0 && n <= 10, "invalid count '%s': must be an integer from 1-10", arg);
25 |
26 | if (n == 0) {
27 | *sz_out = 0;
28 | nm_err_set(NULL);
29 | return NULL;
30 | }
31 |
32 | nm_menu_item_t **items = calloc(n, sizeof(nm_menu_item_t*));
33 | for (size_t i = 0; i < (size_t)(n); i++) {
34 | items[i] = calloc(1, sizeof(nm_menu_item_t));
35 | items[i]->action = calloc(1, sizeof(nm_menu_action_t));
36 | asprintf(&items[i]->lbl, "Generated %zu", i+1);
37 | items[i]->action->act = NM_ACTION(dbg_msg);
38 | items[i]->action->arg = strdup("Pressed");
39 | items[i]->action->on_failure = true;
40 | items[i]->action->on_success = true;
41 | }
42 |
43 | clock_gettime(CLOCK_REALTIME, time_in_out); // note: any nonzero value would work, but this generator is for testing and as an example
44 |
45 | *sz_out = n;
46 | nm_err_set(NULL);
47 | return items;
48 | }
49 |
50 | NM_GENERATOR_(_test_time) {
51 | if (arg && *arg)
52 | NM_ERR_RET(NULL, "_test_time does not accept any arguments");
53 |
54 | // note: this used as an example and for testing
55 |
56 | NM_LOG("_test_time: checking for updates");
57 |
58 | struct timespec ts;
59 | clock_gettime(CLOCK_REALTIME, &ts);
60 |
61 | struct tm lt;
62 | localtime_r(&ts.tv_sec, <);
63 |
64 | if (time_in_out->tv_sec && ts.tv_sec - time_in_out->tv_sec < 10) {
65 | NM_LOG("_test_time: last update is nonzero and last update time is < 10s, skipping");
66 | nm_err_set(NULL);
67 | return NULL;
68 | }
69 |
70 | NM_LOG("_test_time: updating");
71 |
72 | // note: you'd usually do the slower logic here
73 |
74 | nm_menu_item_t **items = calloc(1, sizeof(nm_menu_item_t*));
75 | items[0] = calloc(1, sizeof(nm_menu_item_t));
76 | asprintf(&items[0]->lbl, "%d:%02d:%02d", lt.tm_hour, lt.tm_min, lt.tm_sec);
77 | items[0]->action = calloc(1, sizeof(nm_menu_action_t));
78 | items[0]->action->act = NM_ACTION(dbg_msg);
79 | items[0]->action->arg = strdup("It worked!");
80 | items[0]->action->on_failure = true;
81 | items[0]->action->on_success = true;
82 |
83 | time_in_out->tv_sec = ts.tv_sec;
84 |
85 | *sz_out = 1;
86 | nm_err_set(NULL);
87 | return items;
88 | }
89 |
90 | NM_GENERATOR_(kfmon) {
91 | struct stat sb;
92 | if (stat(KFMON_IPC_SOCKET, &sb))
93 | NM_ERR_RET(NULL, "error checking '%s': stat: %m", KFMON_IPC_SOCKET);
94 |
95 | if (time_in_out->tv_sec == sb.st_mtim.tv_sec && time_in_out->tv_nsec == sb.st_mtim.tv_nsec) {
96 | nm_err_set(NULL);
97 | return NULL;
98 | }
99 |
100 | // Default with no arg or an empty arg is to request a gui-listing
101 | const char *kfmon_cmd = NULL;
102 | if (!arg || !*arg || !strcmp(arg, "gui")) {
103 | kfmon_cmd = "gui-list";
104 | } else if (!strcmp(arg, "all")) {
105 | kfmon_cmd = "list";
106 | } else {
107 | NM_ERR_RET(NULL, "invalid argument '%s': if specified, must be either gui or all", arg);
108 | }
109 |
110 | // We'll want to retrieve our watch list in there.
111 | kfmon_watch_list_t list = { 0 };
112 | int status = nm_kfmon_list_request(kfmon_cmd, &list);
113 |
114 | // If there was an error, handle it now.
115 | if (nm_kfmon_error_handler(status))
116 | return NULL; // the error will be passed on
117 |
118 | // Handle an empty listing safely
119 | if (list.count == 0) {
120 | *sz_out = 0;
121 | nm_err_set(NULL);
122 | return NULL;
123 | }
124 |
125 | // And now we can start populating an array of nm_menu_item_t :)
126 | *sz_out = list.count;
127 | nm_menu_item_t **items = calloc(list.count, sizeof(nm_menu_item_t*));
128 |
129 | // Walk the list to populate the items array
130 | size_t i = 0;
131 | for (kfmon_watch_node_t *node = list.head; node != NULL; node = node->next) {
132 | items[i] = calloc(1, sizeof(nm_menu_item_t));
133 | items[i]->action = calloc(1, sizeof(nm_menu_action_t));
134 | items[i]->lbl = strdup(node->watch.label);
135 | items[i]->action->act = NM_ACTION(kfmon);
136 | items[i]->action->arg = strdup(node->watch.filename);
137 | items[i]->action->on_failure = true;
138 | items[i]->action->on_success = true;
139 | i++;
140 | }
141 |
142 | // Destroy the list now that we've dumped it into an array of nm_menu_item_t
143 | kfmon_teardown_list(&list);
144 |
145 | *time_in_out = sb.st_mtim;
146 | nm_err_set(NULL);
147 | return items;
148 | }
149 |
--------------------------------------------------------------------------------
/src/kfmon.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | #include "kfmon.h"
14 | #include "kfmon_helpers.h"
15 | #include "util.h"
16 |
17 | // Free all resources allocated by a list and its nodes
18 | inline void kfmon_teardown_list(kfmon_watch_list_t *list) {
19 | kfmon_watch_node_t *node = list->head;
20 | while (node) {
21 | kfmon_watch_node_t *p = node->next;
22 | free(node->watch.filename);
23 | free(node->watch.label);
24 | free(node);
25 | node = p;
26 | }
27 | // Don't leave dangling pointers
28 | list->head = NULL;
29 | list->tail = NULL;
30 | }
31 |
32 | // Allocate a single new node to the list
33 | inline int kfmon_grow_list(kfmon_watch_list_t *list) {
34 | kfmon_watch_node_t *prev = list->tail;
35 | kfmon_watch_node_t *node = calloc(1, sizeof(*node));
36 | if (!node) {
37 | return KFMON_IPC_CALLOC_FAILURE;
38 | }
39 | list->count++;
40 |
41 | // Update the head if this is the first node
42 | if (!list->head) {
43 | list->head = node;
44 | }
45 | // Update the tail pointer
46 | list->tail = node;
47 | // If there was a previous node, link the two together
48 | if (prev) {
49 | prev->next = node;
50 | }
51 |
52 | return EXIT_SUCCESS;
53 | }
54 |
55 | // Handle replies from the IPC socket
56 | static int handle_reply(int data_fd, void *data __attribute__((unused))) {
57 | // Eh, recycle PIPE_BUF, it should be more than enough for our needs.
58 | char buf[PIPE_BUF] = { 0 };
59 |
60 | // We don't actually know the size of the reply, so, best effort here.
61 | ssize_t len = xread(data_fd, buf, sizeof(buf));
62 | if (len < 0) {
63 | // Only actual failures are left, xread handles the rest
64 | return KFMON_IPC_REPLY_READ_FAILURE;
65 | }
66 |
67 | // If there's actually nothing to read (EoF), abort.
68 | if (len == 0) {
69 | return KFMON_IPC_ENODATA;
70 | }
71 |
72 | // Check the reply for failures
73 | if (!strncmp(buf, "ERR_INVALID_ID", 14)) {
74 | return KFMON_IPC_ERR_INVALID_ID;
75 | } else if (!strncmp(buf, "WARN_ALREADY_RUNNING", 20)) {
76 | return KFMON_IPC_WARN_ALREADY_RUNNING;
77 | } else if (!strncmp(buf, "WARN_SPAWN_BLOCKED", 18)) {
78 | return KFMON_IPC_WARN_SPAWN_BLOCKED;
79 | } else if (!strncmp(buf, "WARN_SPAWN_INHIBITED", 20)) {
80 | return KFMON_IPC_WARN_SPAWN_INHIBITED;
81 | } else if (!strncmp(buf, "ERR_REALLY_MALFORMED_CMD", 24)) {
82 | return KFMON_IPC_ERR_REALLY_MALFORMED_CMD;
83 | } else if (!strncmp(buf, "ERR_MALFORMED_CMD", 17)) {
84 | return KFMON_IPC_ERR_MALFORMED_CMD;
85 | } else if (!strncmp(buf, "ERR_INVALID_CMD", 15)) {
86 | return KFMON_IPC_ERR_INVALID_CMD;
87 | } else if (!strncmp(buf, "OK", 2)) {
88 | return EXIT_SUCCESS;
89 | } else {
90 | return KFMON_IPC_UNKNOWN_REPLY;
91 | }
92 |
93 | // We're not done until we've got a reply we're satisfied with...
94 | return KFMON_IPC_EAGAIN;
95 | }
96 |
97 | // Handle replies from a 'list' command
98 | static int handle_list_reply(int data_fd, void *data) {
99 | // Can't do it on the stack because of strsep
100 | char *buf = NULL;
101 | buf = calloc(PIPE_BUF, sizeof(*buf));
102 | if (!buf) {
103 | return KFMON_IPC_CALLOC_FAILURE;
104 | }
105 |
106 | // Until proven otherwise...
107 | int status = EXIT_SUCCESS;
108 |
109 | // We don't actually know the size of the reply, so, best effort here.
110 | ssize_t len = xread(data_fd, buf, PIPE_BUF);
111 | if (len < 0) {
112 | // Only actual failures are left, xread handles the rest
113 | status = KFMON_IPC_REPLY_READ_FAILURE;
114 | goto cleanup;
115 | }
116 |
117 | // If there's actually nothing to read (EoF), abort.
118 | if (len == 0) {
119 | status = KFMON_IPC_ENODATA;
120 | goto cleanup;
121 | }
122 |
123 | // The only valid reply for list is... a list ;).
124 | if (!strncmp(buf, "ERR_INVALID_CMD", 15)) {
125 | status = KFMON_IPC_ERR_INVALID_CMD;
126 | goto cleanup;
127 | } else if ((!strncmp(buf, "WARN_", 5)) ||
128 | (!strncmp(buf, "ERR_", 4)) ||
129 | (!strncmp(buf, "OK", 2))) {
130 | status = KFMON_IPC_UNKNOWN_REPLY;
131 | goto cleanup;
132 | }
133 |
134 | // NOTE: Replies may be split across multiple reads (and as such, multiple handle_list_reply calls).
135 | // So the only way we can be sure that we're done (short of timing out after a while of no POLLIN revents,
136 | // which would be stupid), is to check that the final byte we've just read is a NUL,
137 | // as that's how KFMon terminates a list.
138 | bool eot = false;
139 | // NOTE: The parsing code later does its own take on this to detect the final line,
140 | // but this one should be authoritative, as strsep modifies the data.
141 | if (buf[len - 1] == '\0') {
142 | eot = true;
143 | }
144 |
145 | // Keep some minimal debug logging around, just in case...
146 | NM_LOG("Got a %zd bytes reply from KFMon (%s an EoT marker)", len, eot ? "with" : "*without*");
147 | // Now that we're sure we didn't get a wonky reply from an unrelated command, parse the list
148 | // NOTE: Format is:
149 | // id:filename:label or id:filename for watches without a label
150 | // We don't care about id, as it potentially won't be stable across the full powercycle,
151 | // filename is what we pass verbatim to a kfmon action
152 | // label is our action's lbl (use filename if NULL)
153 | char *p = buf;
154 | char *line = NULL;
155 | // Break the reply line by line
156 | while ((line = strsep(&p, "\n")) != NULL) {
157 | // Then parse each line...
158 | // If it's the final line, its only content is a single NUL
159 | if (*line == '\0') {
160 | // NOTE: This might also simply be the end of a single-line read,
161 | // in which case the NUL is thanks to calloc...
162 | break;
163 | }
164 | NM_LOG("Parsing reply line: `%s`", line);
165 | // NOTE: Simple syslog logging for now
166 | char *watch_idx = strsep(&line, ":");
167 | if (!watch_idx) {
168 | status = KFMON_IPC_LIST_PARSE_FAILURE;
169 | goto cleanup;
170 | }
171 | char *filename = strsep(&line, ":");
172 | if (!filename) {
173 | status = KFMON_IPC_LIST_PARSE_FAILURE;
174 | goto cleanup;
175 | }
176 | // Final separator is optional, if it's not there, there's no label, use the filename instead.
177 | char *label = strsep(&line, ":");
178 |
179 | // Store that at the tail of the list
180 | kfmon_watch_list_t *list = (kfmon_watch_list_t*) data;
181 | // Make room for a new node
182 | if (kfmon_grow_list(list) != EXIT_SUCCESS) {
183 | status = KFMON_IPC_CALLOC_FAILURE;
184 | goto cleanup;
185 | }
186 | // Use it
187 | kfmon_watch_node_t *node = list->tail;
188 |
189 | node->watch.idx = (uint8_t) strtoul(watch_idx, NULL, 10);
190 | node->watch.filename = strdup(filename);
191 | node->watch.label = label ? strdup(label) : strdup(filename);
192 | }
193 |
194 | // Are we really done?
195 | status = eot ? EXIT_SUCCESS : KFMON_IPC_EAGAIN;
196 |
197 | cleanup:
198 | free(buf);
199 | return status;
200 | }
201 |
202 | // Connect to KFMon's IPC socket. Returns error code, store data fd by ref.
203 | static int connect_to_kfmon_socket(int *restrict data_fd) {
204 | // Setup the local socket
205 | if ((*data_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)) == -1) {
206 | return KFMON_IPC_SOCKET_FAILURE;
207 | }
208 |
209 | struct sockaddr_un sock_name = { 0 };
210 | sock_name.sun_family = AF_UNIX;
211 | strncpy(sock_name.sun_path, KFMON_IPC_SOCKET, sizeof(sock_name.sun_path) - 1);
212 |
213 | // Connect to IPC socket, retrying safely on EINTR (c.f., http://www.madore.org/~david/computers/connect-intr.html)
214 | while (connect(*data_fd, (const struct sockaddr*) &sock_name, sizeof(sock_name)) == -1 && errno != EISCONN) {
215 | if (errno != EINTR) {
216 | return KFMON_IPC_CONNECT_FAILURE;
217 | }
218 | }
219 |
220 | // Wheee!
221 | return EXIT_SUCCESS;
222 | }
223 |
224 | // Send a packet to KFMon over the wire (payload *MUST* be NUL-terminated to avoid truncation, and len *MUST* include that NUL).
225 | static int send_packet(int data_fd, const char *restrict payload, size_t len) {
226 | // Send it (w/ a NUL)
227 | if (send_in_full(data_fd, payload, len) < 0) {
228 | // Only actual failures are left
229 | if (errno == EPIPE) {
230 | return KFMON_IPC_EPIPE;
231 | } else {
232 | return KFMON_IPC_SEND_FAILURE;
233 | }
234 | }
235 |
236 | // Wheee!
237 | return EXIT_SUCCESS;
238 | }
239 |
240 | // Send the requested IPC command:arg pair (or command alone if arg is NULL)
241 | static int send_ipc_command(int data_fd, const char *restrict ipc_cmd, const char *restrict ipc_arg) {
242 | char buf[256] = { 0 };
243 | int packet_len = 0;
244 | // Somme commands don't require an arg
245 | if (ipc_arg) {
246 | packet_len = snprintf(buf, sizeof(buf), "%s:%s", ipc_cmd, ipc_arg);
247 | } else {
248 | packet_len = snprintf(buf, sizeof(buf), "%s", ipc_cmd);
249 | }
250 | // Send it (w/ a NUL)
251 | return send_packet(data_fd, buf, (size_t) (packet_len + 1));
252 | }
253 |
254 | // Poll the IPC socket for potentially *multiple* replies to a single command, timeout after attempts * timeout (ms)
255 | static int wait_for_replies(int data_fd, int timeout, size_t attempts, ipc_handler_t reply_handler, void **data) {
256 | int status = EXIT_SUCCESS;
257 |
258 | struct pollfd pfd = { 0 };
259 | // Data socket
260 | pfd.fd = data_fd;
261 | pfd.events = POLLIN;
262 |
263 | // Here goes... We'll wait for windows of ms
264 | size_t retry = 0U;
265 | while (1) {
266 | int poll_num = poll(&pfd, 1, timeout);
267 | if (poll_num == -1) {
268 | if (errno == EINTR) {
269 | continue;
270 | }
271 | return KFMON_IPC_POLL_FAILURE;
272 | }
273 |
274 | if (poll_num > 0) {
275 | if (pfd.revents & POLLIN) {
276 | // There was a reply from the socket
277 | int reply = reply_handler(data_fd, data);
278 | if (reply != EXIT_SUCCESS) {
279 | // If the remote closed the connection, we get POLLIN|POLLHUP w/ EoF ;).
280 | if (pfd.revents & POLLHUP) {
281 | // Flag that as an error
282 | status = KFMON_IPC_EPIPE;
283 | } else {
284 | if (reply == KFMON_IPC_EAGAIN) {
285 | // We're expecting more stuff to read, keep going.
286 | continue;
287 | } else {
288 | // Something went wrong when handling the reply, pass the error as-is
289 | status = reply;
290 | }
291 | }
292 | // We're obviously done if something went wrong.
293 | break;
294 | } else {
295 | // We break on success, too, as we only need to send a single command.
296 | status = EXIT_SUCCESS;
297 | break;
298 | }
299 | }
300 |
301 | // Remote closed the connection
302 | if (pfd.revents & POLLHUP) {
303 | // Flag that as an error
304 | status = KFMON_IPC_EPIPE;
305 | break;
306 | }
307 | }
308 |
309 | if (poll_num == 0) {
310 | // Timed out, increase the retry counter
311 | retry++;
312 | }
313 |
314 | // Drop the axe after the final attempt
315 | if (retry >= attempts) {
316 | status = KFMON_IPC_ETIMEDOUT;
317 | break;
318 | }
319 | }
320 |
321 | return status;
322 | }
323 |
324 | // Handle a simple KFMon IPC request
325 | int nm_kfmon_simple_request(const char *restrict ipc_cmd, const char *restrict ipc_arg) {
326 | // Assume everything's peachy until shit happens...
327 | int status = EXIT_SUCCESS;
328 |
329 | int data_fd = -1;
330 | // Attempt to connect to KFMon...
331 | // As long as KFMon is up, has very little chance to fail, even if the connection backlog is full.
332 | status = connect_to_kfmon_socket(&data_fd);
333 | // If it failed, return early
334 | if (status != EXIT_SUCCESS) {
335 | return status;
336 | }
337 |
338 | // Attempt to send the specified command in full over the wire
339 | status = send_ipc_command(data_fd, ipc_cmd, ipc_arg);
340 | // If it failed, return early, after closing the socket
341 | if (status != EXIT_SUCCESS) {
342 | close(data_fd);
343 | return status;
344 | }
345 |
346 | // We'll be polling the socket for a reply, this'll make things neater, and allows us to abort on timeout,
347 | // in the unlikely event there's already an IPC session being handled by KFMon,
348 | // in which case the reply would be delayed by an undeterminate amount of time (i.e., until KFMon gets to it).
349 | // Here, we'll want to timeout after 2s
350 | ipc_handler_t handler = &handle_reply;
351 | status = wait_for_replies(data_fd, 500, 4, handler, NULL);
352 | // NOTE: We happen to be done with the connection right now.
353 | // But if we still needed it, KFMON_IPC_POLL_FAILURE would warrant an early abort w/ a forced close().
354 |
355 | // Bye now!
356 | close(data_fd);
357 | return status;
358 | }
359 |
360 | // Handle a list request for the KFMon generator
361 | int nm_kfmon_list_request(const char *restrict ipc_cmd, kfmon_watch_list_t *list) {
362 | // Assume everything's peachy until shit happens...
363 | int status = EXIT_SUCCESS;
364 |
365 | int data_fd = -1;
366 | // Attempt to connect to KFMon...
367 | // As long as KFMon is up, has very little chance to fail, even if the connection backlog is full.
368 | status = connect_to_kfmon_socket(&data_fd);
369 | // If it failed, return early
370 | if (status != EXIT_SUCCESS) {
371 | return status;
372 | }
373 |
374 | // Attempt to send the specified command in full over the wire
375 | status = send_ipc_command(data_fd, ipc_cmd, NULL);
376 | // If it failed, return early, after closing the socket
377 | if (status != EXIT_SUCCESS) {
378 | close(data_fd);
379 | return status;
380 | }
381 |
382 | // We'll be polling the socket for a reply, this'll make things neater, and allows us to abort on timeout,
383 | // in the unlikely event there's already an IPC session being handled by KFMon,
384 | // in which case the reply would be delayed by an undeterminate amount of time (i.e., until KFMon gets to it).
385 | // Here, we'll want to timeout after 2s
386 | ipc_handler_t handler = &handle_list_reply;
387 | status = wait_for_replies(data_fd, 500, 4, handler, (void *) list);
388 | // NOTE: We happen to be done with the connection right now.
389 | // But if we still needed it, KFMON_IPC_POLL_FAILURE would warrant an early abort w/ a forced close().
390 |
391 | // Bye now!
392 | close(data_fd);
393 | return status;
394 | }
395 |
396 | // Giant ladder of fail
397 | bool nm_kfmon_error_handler(kfmon_ipc_errno_e status) {
398 | switch (status) {
399 | case KFMON_IPC_OK:
400 | return nm_err_set(NULL);
401 | // Fail w/ the right log message
402 | case KFMON_IPC_ETIMEDOUT:
403 | return nm_err_set("Timed out waiting for KFMon");
404 | case KFMON_IPC_EPIPE:
405 | return nm_err_set("KFMon closed the connection");
406 | case KFMON_IPC_ENODATA:
407 | return nm_err_set("No more data to read");
408 | case KFMON_IPC_READ_FAILURE:
409 | // NOTE: Let's hope close() won't mangle errno...
410 | return nm_err_set("read: %m");
411 | case KFMON_IPC_SEND_FAILURE:
412 | // NOTE: Let's hope close() won't mangle errno...
413 | return nm_err_set("send: %m");
414 | case KFMON_IPC_SOCKET_FAILURE:
415 | return nm_err_set("Failed to create local KFMon IPC socket (socket: %m)");
416 | case KFMON_IPC_CONNECT_FAILURE:
417 | return nm_err_set("KFMon IPC is down (connect: %m)");
418 | case KFMON_IPC_POLL_FAILURE:
419 | // NOTE: Let's hope close() won't mangle errno...
420 | return nm_err_set("poll: %m");
421 | case KFMON_IPC_CALLOC_FAILURE:
422 | return nm_err_set("calloc: %m");
423 | case KFMON_IPC_REPLY_READ_FAILURE:
424 | // NOTE: Let's hope close() won't mangle errno...
425 | return nm_err_set("Failed to read KFMon's reply (%m)");
426 | case KFMON_IPC_LIST_PARSE_FAILURE:
427 | return nm_err_set("Failed to parse the list of watches (no separator found)");
428 | case KFMON_IPC_ERR_INVALID_ID:
429 | return nm_err_set("Requested to start an invalid watch index");
430 | case KFMON_IPC_ERR_INVALID_NAME:
431 | return nm_err_set("Requested to trigger an invalid watch filename (expected the basename of the image trigger)");
432 | case KFMON_IPC_WARN_ALREADY_RUNNING:
433 | return nm_err_set("Requested watch is already running");
434 | case KFMON_IPC_WARN_SPAWN_BLOCKED:
435 | return nm_err_set("A spawn blocker is currently running");
436 | case KFMON_IPC_WARN_SPAWN_INHIBITED:
437 | return nm_err_set("Spawns are currently inhibited");
438 | case KFMON_IPC_ERR_REALLY_MALFORMED_CMD:
439 | return nm_err_set("KFMon couldn't parse our command");
440 | case KFMON_IPC_ERR_MALFORMED_CMD:
441 | return nm_err_set("Bad command syntax");
442 | case KFMON_IPC_ERR_INVALID_CMD:
443 | return nm_err_set("Command wasn't recognized by KFMon");
444 | case KFMON_IPC_UNKNOWN_REPLY:
445 | return nm_err_set("We couldn't make sense of KFMon's reply");
446 | case KFMON_IPC_EAGAIN:
447 | default:
448 | // Should never happen
449 | return nm_err_set("Something went wrong");
450 | }
451 | }
452 |
453 | nm_action_result_t *nm_kfmon_return_handler(kfmon_ipc_errno_e status) {
454 | if (!nm_kfmon_error_handler(status))
455 | return nm_action_result_silent();
456 | return NULL;
457 | }
458 |
--------------------------------------------------------------------------------
/src/kfmon.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_KFMON_H
2 | #define NM_KFMON_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include "action.h"
12 |
13 | // Path to KFMon's IPC Unix socket
14 | #define KFMON_IPC_SOCKET "/tmp/kfmon-ipc.ctl"
15 |
16 | // Flags for the failure bingo
17 | typedef enum {
18 | // Not an error ;p
19 | KFMON_IPC_OK = EXIT_SUCCESS,
20 | // NOTE: Start > 256 to stay clear of errno
21 | KFMON_IPC_ETIMEDOUT = 512,
22 | KFMON_IPC_EPIPE,
23 | KFMON_IPC_ENODATA,
24 | // syscall failures
25 | KFMON_IPC_READ_FAILURE,
26 | KFMON_IPC_SEND_FAILURE,
27 | KFMON_IPC_SOCKET_FAILURE,
28 | KFMON_IPC_CONNECT_FAILURE,
29 | KFMON_IPC_POLL_FAILURE,
30 | KFMON_IPC_CALLOC_FAILURE,
31 | KFMON_IPC_REPLY_READ_FAILURE,
32 | KFMON_IPC_LIST_PARSE_FAILURE,
33 | // Those match the actual string sent over the wire
34 | KFMON_IPC_ERR_INVALID_ID,
35 | KFMON_IPC_ERR_INVALID_NAME,
36 | KFMON_IPC_WARN_ALREADY_RUNNING,
37 | KFMON_IPC_WARN_SPAWN_BLOCKED,
38 | KFMON_IPC_WARN_SPAWN_INHIBITED,
39 | KFMON_IPC_ERR_REALLY_MALFORMED_CMD,
40 | KFMON_IPC_ERR_MALFORMED_CMD,
41 | KFMON_IPC_ERR_INVALID_CMD,
42 | KFMON_IPC_UNKNOWN_REPLY,
43 | // Not an error either, means we have more to read...
44 | KFMON_IPC_EAGAIN,
45 | } kfmon_ipc_errno_e;
46 |
47 | // A single watch item
48 | typedef struct {
49 | uint8_t idx;
50 | char *filename;
51 | char *label;
52 | } kfmon_watch_t;
53 |
54 | // A node in a linked list of watches
55 | typedef struct kfmon_watch_node {
56 | kfmon_watch_t watch;
57 | struct kfmon_watch_node *next;
58 | } kfmon_watch_node_t;
59 |
60 | // A control structure to keep track of a list of watches
61 | typedef struct {
62 | size_t count;
63 | kfmon_watch_node_t *head;
64 | kfmon_watch_node_t *tail;
65 | } kfmon_watch_list_t;
66 |
67 | // Used as the reply handler in our polling loops.
68 | // Second argument is an opaque pointer used for storage in a linked list
69 | // (e.g., a pointer to a kfmon_watch_list_t, or NULL if no storage is needed).
70 | typedef int (*ipc_handler_t)(int, void *);
71 |
72 | // Free all resources allocated by a list and its nodes
73 | void kfmon_teardown_list(kfmon_watch_list_t *list);
74 | // Allocate a single new node to the list
75 | int kfmon_grow_list(kfmon_watch_list_t *list);
76 |
77 | // If status is success, false is returned. Otherwise, true is returned and
78 | // nm_err is set.
79 | bool nm_kfmon_error_handler(kfmon_ipc_errno_e status);
80 |
81 | // Given one of the error codes listed above, return properly from an action.
82 | nm_action_result_t *nm_kfmon_return_handler(kfmon_ipc_errno_e status);
83 |
84 | // Send a simple KFMon IPC request, one where the reply is only used for its diagnostic value.
85 | int nm_kfmon_simple_request(const char *restrict ipc_cmd, const char *restrict ipc_arg);
86 |
87 | // Handle a list request for the KFMon generator
88 | int nm_kfmon_list_request(const char *restrict ipc_cmd, kfmon_watch_list_t *list);
89 |
90 | #ifdef __cplusplus
91 | }
92 | #endif
93 | #endif
94 |
--------------------------------------------------------------------------------
/src/kfmon_helpers.h:
--------------------------------------------------------------------------------
1 | /* $OpenBSD: atomicio.c,v 1.30 2019/01/24 02:42:23 dtucker Exp $ */
2 | /*
3 | * Copyright (c) 2006 Damien Miller. All rights reserved.
4 | * Copyright (c) 2005 Anil Madhavapeddy. All rights reserved.
5 | * Copyright (c) 1995,1999 Theo de Raadt. All rights reserved.
6 | * All rights reserved.
7 | * SPDX-License-Identifier: BSD-2-Clause
8 | *
9 | * Redistribution and use in source and binary forms, with or without
10 | * modification, are permitted provided that the following conditions
11 | * are met:
12 | * 1. Redistributions of source code must retain the above copyright
13 | * notice, this list of conditions and the following disclaimer.
14 | * 2. Redistributions in binary form must reproduce the above copyright
15 | * notice, this list of conditions and the following disclaimer in the
16 | * documentation and/or other materials provided with the distribution.
17 | *
18 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 | */
29 |
30 | // NOTE: Originally imported from https://github.com/openssh/openssh-portable/blob/master/atomicio.c
31 | // Rejigged for my own use, with inspiration from git's own read/write wrappers,
32 | // as well as gnulib's and busybox's
33 | // c.f., https://github.com/git/git/blob/master/wrapper.c
34 | // https://git.savannah.gnu.org/cgit/gnulib.git/tree/lib/safe-read.c
35 | // https://git.savannah.gnu.org/cgit/gnulib.git/tree/lib/full-write.c
36 | // https://git.busybox.net/busybox/tree/libbb/read.c
37 |
38 | #ifndef NM_KFMON_HELPERS_H
39 | #define NM_KFMON_HELPERS_H
40 | #ifdef __cplusplus
41 | extern "C" {
42 | #endif
43 |
44 | #include
45 | #include
46 | #include
47 | #include
48 | #include
49 | #include
50 |
51 | // Clamp IO chunks to the smallest of 8 MiB and SSIZE_MAX,
52 | // to deal with various implementation quirks on really old Linux,
53 | // macOS, or AIX/IRIX.
54 | // c.f., git, gnulib & busybox for similar stories.
55 | // Since we ourselves are 32 bit Linux-bound, 8 MiB suits us just fine.
56 | #define MAX_IO_BUFSIZ (8 * 1024 * 1024)
57 | #if defined(SSIZE_MAX) && (SSIZE_MAX < MAX_IO_BUFSIZ)
58 | # undef MAX_IO_BUFSIZ
59 | # define MAX_IO_BUFSIZ SSIZE_MAX
60 | #endif
61 |
62 | // read() with retries on recoverable errors (via polling on EAGAIN).
63 | // Not guaranteed to return len bytes, even on success (like read() itself).
64 | // Always returns read()'s return value as-is.
65 | static ssize_t xread(int fd, void *buf, size_t len) {
66 | // Save a trip to EINVAL if len is large enough to make read() fail.
67 | if (len > MAX_IO_BUFSIZ) {
68 | len = MAX_IO_BUFSIZ;
69 | }
70 |
71 | while (1) {
72 | ssize_t nr = read(fd, buf, len);
73 | if (nr < 0) {
74 | if (errno == EINTR) {
75 | continue;
76 | } else if (errno == EAGAIN) {
77 | struct pollfd pfd = { 0 };
78 | pfd.fd = fd;
79 | pfd.events = POLLIN;
80 |
81 | poll(&pfd, 1, -1);
82 | continue;
83 | }
84 | }
85 | return nr;
86 | }
87 | }
88 |
89 | // write() with retries on recoverable errors (via polling on EAGAIN).
90 | // Not guaranteed to write len bytes, even on success (like write() itself).
91 | // Always returns write()'s return value as-is.
92 | static __attribute__((unused)) ssize_t xwrite(int fd, const void *buf, size_t len) {
93 | // Save a trip to EINVAL if len is large enough to make write() fail.
94 | if (len > MAX_IO_BUFSIZ) {
95 | len = MAX_IO_BUFSIZ;
96 | }
97 |
98 | while (1) {
99 | ssize_t nw = write(fd, buf, len);
100 | if (nw < 0) {
101 | if (errno == EINTR) {
102 | continue;
103 | } else if (errno == EAGAIN) {
104 | struct pollfd pfd = { 0 };
105 | pfd.fd = fd;
106 | pfd.events = POLLOUT;
107 |
108 | poll(&pfd, 1, -1);
109 | continue;
110 | }
111 | }
112 | return nw;
113 | }
114 | }
115 |
116 | // Based on OpenSSH's atomicio6, except we keep the return value/data type of the original call.
117 | // Ensure all of data on socket comes through.
118 | static __attribute__((unused)) ssize_t read_in_full(int fd, void *buf, size_t len) {
119 | // Save a trip to EINVAL if len is large enough to make read() fail.
120 | if (len > MAX_IO_BUFSIZ) {
121 | len = MAX_IO_BUFSIZ;
122 | }
123 |
124 | char *s = buf;
125 | size_t pos = 0U;
126 | while (len > pos) {
127 | ssize_t nr = read(fd, s + pos, len - pos);
128 | switch (nr) {
129 | case -1:
130 | if (errno == EINTR) {
131 | continue;
132 | } else if (errno == EAGAIN) {
133 | struct pollfd pfd = { 0 };
134 | pfd.fd = fd;
135 | pfd.events = POLLIN;
136 |
137 | poll(&pfd, 1, -1);
138 | continue;
139 | }
140 | return -1;
141 | case 0:
142 | // i.e., EoF/EoT
143 | errno = EPIPE;
144 | return (ssize_t) pos;
145 | default:
146 | pos += (size_t) nr;
147 | }
148 | }
149 | return (ssize_t) pos;
150 | }
151 |
152 | static __attribute__((unused)) ssize_t write_in_full(int fd, const void *buf, size_t len) {
153 | // Save a trip to EINVAL if len is large enough to make write() fail.
154 | if (len > MAX_IO_BUFSIZ) {
155 | len = MAX_IO_BUFSIZ;
156 | }
157 |
158 | const char *s = buf;
159 | size_t pos = 0U;
160 | while (len > pos) {
161 | ssize_t nw = write(fd, s + pos, len - pos);
162 | switch (nw) {
163 | case -1:
164 | if (errno == EINTR) {
165 | continue;
166 | } else if (errno == EAGAIN) {
167 | struct pollfd pfd = { 0 };
168 | pfd.fd = fd;
169 | pfd.events = POLLOUT;
170 |
171 | poll(&pfd, 1, -1);
172 | continue;
173 | }
174 | return -1;
175 | case 0:
176 | // That only makes sense for regular files.
177 | // On the other hand, write() returning 0 on !regular files is UB.
178 | errno = ENOSPC;
179 | return -1;
180 | default:
181 | pos += (size_t) nw;
182 | }
183 | }
184 | return (ssize_t) pos;
185 | }
186 |
187 | // Exactly like write_in_full, but using send w/ flags set to MSG_NOSIGNAL,
188 | // so we can handle EPIPE without having to deal with signals.
189 | static ssize_t send_in_full(int sockfd, const void *buf, size_t len) {
190 | // Save a trip to EINVAL if len is large enough to make send() fail.
191 | if (len > MAX_IO_BUFSIZ) {
192 | len = MAX_IO_BUFSIZ;
193 | }
194 |
195 | const char *s = buf;
196 | size_t pos = 0U;
197 | while (len > pos) {
198 | ssize_t nw = send(sockfd, s + pos, len - pos, MSG_NOSIGNAL);
199 | switch (nw) {
200 | case -1:
201 | if (errno == EINTR) {
202 | continue;
203 | } else if (errno == EAGAIN) {
204 | struct pollfd pfd = { 0 };
205 | pfd.fd = sockfd;
206 | pfd.events = POLLOUT;
207 |
208 | poll(&pfd, 1, -1);
209 | continue;
210 | }
211 | return -1;
212 | default:
213 | pos += (size_t) nw;
214 | }
215 | }
216 | return (ssize_t) pos;
217 | }
218 |
219 | #ifdef __cplusplus
220 | }
221 | #endif
222 | #endif
223 |
--------------------------------------------------------------------------------
/src/nickelmenu.cc:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include
15 |
16 | #include
17 |
18 | #include "action.h"
19 | #include "config.h"
20 | #include "nickelmenu.h"
21 | #include "util.h"
22 |
23 | typedef QWidget MenuTextItem; // it's actually a subclass, but we don't need its functionality directly, so we'll stay on the safe side
24 | typedef void MainWindowController;
25 |
26 | // AbstractNickelMenuController::createMenuTextItem creates a menu item
27 | // (allocated on the heap). The last parameter has been empty everywhere I've
28 | // seen it. The checked option does not have any effect unless checkable is true
29 | // (see the search menu for an example).
30 | static MenuTextItem* (*AbstractNickelMenuController_createMenuTextItem)(void*, QMenu* menu, QString const& text, bool checkable, bool checked, QString const& /*I think it's for QObject::setObjectName*/);
31 |
32 | // AbstractNickelMenuController::createAction finishes adding the action to the
33 | // menu. The actionWidget should be a MenuTextItem (allocated on the heap). If
34 | // disabled is true, the item will appear greyed out. Note that for the
35 | // separator, even in the main menu (removed in FW 15505), it'll inherit the
36 | // color of a Reader Menu separator (#666), rather than the usual dim (#BBB) or
37 | // solid (#000) ones seen in the stock main menu.
38 | //
39 | // The code is basically:
40 | // act = new QWidgetAction(this, arg2);
41 | // act->setEnabled(arg4);
42 | // if (arg3)
43 | // connect(act, &QAction::triggered, &QWidget::hide, arg1);
44 | // arg1->addAction(act);
45 | // if (arg5)
46 | // arg1->addSeparator();
47 | static QAction* (*AbstractNickelMenuController_createAction)(void*, QMenu* menu, QWidget* actionWidget, bool closeOnTap, bool disabled, bool separatorAfter);
48 |
49 | // ConfirmationDialogFactory::showOKDialog shows an dialog box with an OK
50 | // button, and should only be called from the main thread (or a signal handler).
51 | // Note that ConfirmationDialog uses Qt::RichText for the body.
52 | static void (*ConfirmationDialogFactory_showOKDialog)(QString const& title, QString const& body);
53 |
54 | MainWindowController *(*MainWindowController_sharedInstance)();
55 |
56 | // MainWindowController::toast shows a message as an overlay, and should only be
57 | // called from the main thread (or a signal handler).
58 | void (*MainWindowController_toast)(MainWindowController*, QString const& primary, QString const& secondary, int milliseconds);
59 |
60 | // *MenuSeparator::*MenuSeparator initializes a main menu separator which can be
61 | // added to the menu with QWidget::addAction. The menu should be its parent. It
62 | // initializes a QAction.
63 | static void (*LightMenuSeparator_LightMenuSeparator)(void*, QWidget* parent);
64 | static void (*BoldMenuSeparator_BoldMenuSeparator)(void*, QWidget* parent);
65 |
66 | // New bottom tab bar which replaced the main menu on 15505+.
67 | typedef QWidget MainNavButton;
68 | typedef QWidget MainNavView;
69 | void (*MainNavView_MainNavView)(MainNavView*, QWidget* parent);
70 | void (*MainNavButton_MainNavButton)(MainNavButton*, QWidget* parent);
71 | void (*MainNavButton_setPixmap)(MainNavButton*, QString const& pixmapName);
72 | void (*MainNavButton_setActivePixmap)(MainNavButton*, QString const& pixmapName);
73 | void (*MainNavButton_setText)(MainNavButton*, QString const& text);
74 | void (*MainNavButton_tapped)(MainNavButton*); // signal
75 |
76 | // Creating menus from scratch (for the menu above on 15505+).
77 | typedef QMenu TouchMenu;
78 | typedef TouchMenu NickelTouchMenu;
79 | typedef int DecorationPosition;
80 | void (*NickelTouchMenu_NickelTouchMenu)(NickelTouchMenu*, QWidget* parent, DecorationPosition position);
81 | void (*MenuTextItem_MenuTextItem)(MenuTextItem*, QWidget* parent, bool checkable, bool italic);
82 | void (*MenuTextItem_setText)(MenuTextItem*, QString const& text);
83 | void (*MenuTextItem_registerForTapGestures)(MenuTextItem*);
84 |
85 | // Selection menu stuff (14622+).
86 | typedef void SelectionMenuController; // note: items are re-initialized every time the menu is opened
87 | typedef QWidget SelectionMenuView;
88 | typedef void WebSearchMixinBase;
89 | void (*SelectionMenuController_lookupWikipedia)(SelectionMenuController*);
90 | void (*SelectionMenuController_lookupWeb)(SelectionMenuController*); // 14622-18838
91 | void (*SelectionMenuController_lookupGoogle)(SelectionMenuController*); // 19086+ (replaces lookupWeb, alternative 1)
92 | void (*SelectionMenuController_lookupBaidu)(SelectionMenuController*); // 19086+ (replaces lookupWeb, alternative 2)
93 | void (*SelectionMenuController_addMenuItem)(SelectionMenuController*, SelectionMenuView* smv, MenuTextItem* mti, const char *slot); // note: the MenuTextItem is created by SelectionMenuController_createMenuTextItem, with smv as the parent (the first QWidget* argument)
94 | void (*SelectionMenuView_addMenuItem)(SelectionMenuView*, MenuTextItem *mti); // note: this adds the separator and the item (it doesn't connect signals or things like that)
95 | void (*WebSearchMixinBase_doWikipediaSearch)(WebSearchMixinBase *, QString const& selection, QString const& locale);
96 |
97 | static struct nh_info NickelMenu = (struct nh_info){
98 | .name = "NickelMenu",
99 | .desc = "Integrated launcher for Nickel.",
100 | .uninstall_flag = NM_CONFIG_DIR "/uninstall",
101 | #ifdef NM_UNINSTALL_CONFIGDIR
102 | .uninstall_xflag = NM_CONFIG_DIR,
103 | #else
104 | .uninstall_xflag = NULL,
105 | #endif
106 | .failsafe_delay = 3,
107 | };
108 |
109 | static struct nh_hook NickelMenuHook[] = {
110 | // menu injection
111 | {.sym = "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_", .sym_new = "_nm_menu_hook", .lib = "libnickel.so.1.0.0", .out = nh_symoutptr(AbstractNickelMenuController_createMenuTextItem)}, //libnickel 4.6 * _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_
112 |
113 | // bottom nav main menu button injection (15505+)
114 | {.sym = "_ZN11MainNavViewC1EP7QWidget", .sym_new = "_nm_menu_hook2", .lib = "libnickel.so.1.0.0", .out = nh_symoutptr(MainNavView_MainNavView), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN11MainNavViewC1EP7QWidget
115 |
116 | // selection menu injection
117 | {.sym = "_ZN23SelectionMenuController11addMenuItemEP17SelectionMenuViewP12MenuTextItemPKc", .sym_new = "_nm_menu_hook3", .lib = "libnickel.so.1.0.0", .out = nh_symoutptr(SelectionMenuController_addMenuItem), .desc = "selection menu injection", .optional = true}, //libnickel 4.20.14622 * _ZN23SelectionMenuController11addMenuItemEP17SelectionMenuViewP12MenuTextItemPKc
118 | {.sym = "_ZN18WebSearchMixinBase17doWikipediaSearchERK7QStringS2_", .sym_new = "_nm_menu_hook4", .lib = "libnickel.so.1.0.0", .out = nh_symoutptr(WebSearchMixinBase_doWikipediaSearch), .desc = "selection menu injection (wikipedia handler)", .optional = true}, //libnickel 4.20.14622 * _ZN18WebSearchMixinBase17doWikipediaSearchERK7QStringS2_
119 |
120 | // null
121 | {0},
122 | };
123 |
124 | static struct nh_dlsym NickelMenuDlsym[] = {
125 | // menu injection
126 | {.name = "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_", .out = nh_symoutptr(AbstractNickelMenuController_createMenuTextItem)}, //libnickel 4.6 * _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_
127 | {.name = "_ZN22AbstractMenuController12createActionEP5QMenuP7QWidgetbbb", .out = nh_symoutptr(AbstractNickelMenuController_createAction)}, //libnickel 4.6 * _ZN22AbstractMenuController12createActionEP5QMenuP7QWidgetbbb
128 | {.name = "_ZN25ConfirmationDialogFactory12showOKDialogERK7QStringS2_", .out = nh_symoutptr(ConfirmationDialogFactory_showOKDialog)}, //libnickel 4.6 * _ZN25ConfirmationDialogFactory12showOKDialogERK7QStringS2_
129 | {.name = "_ZN20MainWindowController14sharedInstanceEv", .out = nh_symoutptr(MainWindowController_sharedInstance)}, //libnickel 4.6 * _ZN20MainWindowController14sharedInstanceEv
130 | {.name = "_ZN20MainWindowController5toastERK7QStringS2_i", .out = nh_symoutptr(MainWindowController_toast)}, //libnickel 4.6 * _ZN20MainWindowController5toastERK7QStringS2_i
131 | {.name = "_ZN18LightMenuSeparatorC2EP7QWidget", .out = nh_symoutptr(LightMenuSeparator_LightMenuSeparator)}, //libnickel 4.6 * _ZN18LightMenuSeparatorC2EP7QWidget
132 | {.name = "_ZN17BoldMenuSeparatorC1EP7QWidget", .out = nh_symoutptr(BoldMenuSeparator_BoldMenuSeparator)}, //libnickel 4.6 * _ZN17BoldMenuSeparatorC1EP7QWidget
133 |
134 | // bottom nav main menu button injection (15505+)
135 | {.name = "_ZN13MainNavButtonC1EP7QWidget", .out = nh_symoutptr(MainNavButton_MainNavButton), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN13MainNavButtonC1EP7QWidget
136 | {.name = "_ZN13MainNavButton9setPixmapERK7QString", .out = nh_symoutptr(MainNavButton_setPixmap), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN13MainNavButton9setPixmapERK7QString
137 | {.name = "_ZN13MainNavButton15setActivePixmapERK7QString", .out = nh_symoutptr(MainNavButton_setActivePixmap), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN13MainNavButton15setActivePixmapERK7QString
138 | {.name = "_ZN13MainNavButton7setTextERK7QString", .out = nh_symoutptr(MainNavButton_setText), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN13MainNavButton7setTextERK7QString
139 | {.name = "_ZN13MainNavButton6tappedEv", .out = nh_symoutptr(MainNavButton_tapped), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN13MainNavButton6tappedEv
140 | {.name = "_ZN15NickelTouchMenuC2EP7QWidget18DecorationPosition", .out = nh_symoutptr(NickelTouchMenu_NickelTouchMenu), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN15NickelTouchMenuC2EP7QWidget18DecorationPosition
141 | {.name = "_ZN12MenuTextItemC1EP7QWidgetbb", .out = nh_symoutptr(MenuTextItem_MenuTextItem), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN12MenuTextItemC1EP7QWidgetbb
142 | {.name = "_ZN12MenuTextItem7setTextERK7QString", .out = nh_symoutptr(MenuTextItem_setText), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN12MenuTextItem7setTextERK7QString
143 | {.name = "_ZN12MenuTextItem22registerForTapGesturesEv", .out = nh_symoutptr(MenuTextItem_registerForTapGestures), .desc = "bottom nav main menu button injection (15505+)", .optional = true}, //libnickel 4.23.15505 * _ZN12MenuTextItem22registerForTapGesturesEv
144 |
145 | // selection menu injection (14622+)
146 | {.name = "_ZN17SelectionMenuView11addMenuItemEP12MenuTextItem", .out = nh_symoutptr(SelectionMenuView_addMenuItem), .desc = "selection menu injection (14622+)", .optional = true}, //libnickel 4.20.14622 * _ZN17SelectionMenuView11addMenuItemEP12MenuTextItem
147 | {.name = "_ZN23SelectionMenuController15lookupWikipediaEv", .out = nh_symoutptr(SelectionMenuController_lookupWikipedia), .desc = "selection menu injection (14622+)", .optional = true}, //libnickel 4.20.14622 * _ZN23SelectionMenuController15lookupWikipediaEv
148 | {.name = "_ZN23SelectionMenuController9lookupWebEv", .out = nh_symoutptr(SelectionMenuController_lookupWeb), .desc = "selection menu injection (14622-18838)", .optional = true}, //libnickel 4.20.14622 4.30.18838 _ZN23SelectionMenuController9lookupWebEv
149 | {.name = "_ZN23SelectionMenuController12lookupGoogleEv", .out = nh_symoutptr(SelectionMenuController_lookupGoogle), .desc = "selection menu injection (19086+, alt 1)", .optional = true}, //libnickel 4.31.19086 * _ZN23SelectionMenuController12lookupGoogleEv
150 | {.name = "_ZN23SelectionMenuController11lookupBaiduEv", .out = nh_symoutptr(SelectionMenuController_lookupBaidu), .desc = "selection menu injection (19086+, alt 2)", .optional = true}, //libnickel 4.31.19086 * _ZN23SelectionMenuController11lookupBaiduEv
151 |
152 | // null
153 | {0},
154 | };
155 |
156 | static int nm_init();
157 |
158 | NickelHook(
159 | .init = &nm_init,
160 | .info = &NickelMenu,
161 | .hook = NickelMenuHook,
162 | .dlsym = NickelMenuDlsym,
163 | )
164 |
165 | // AbstractNickelMenuController_createAction_before wraps
166 | // AbstractNickelMenuController::createAction to use the correct separator for
167 | // the menu location and to match the behaviour of QMenu::insertAction instead
168 | // of QMenu::addAction. It also adds the property nm_action=true to the action
169 | // and separator.
170 | QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_menu_location_t loc, bool last_in_group, void *_this, QMenu *menu, QWidget *widget, bool close, bool enabled, bool separator);
171 |
172 | // nm_argtranform_t transforms an action's argument and returns a new malloc'd
173 | // string. On error, it should return NULL and set nm_err.
174 | typedef char *(*nm_argtransform_t)(void *data, const char *arg);
175 |
176 | // nm_menu_item_do runs a nm_menu_item_t and must be called from the thread of a
177 | // signal handler. argtransform and argtransform_data are optional.
178 | static void nm_menu_item_do(nm_menu_item_t *it, nm_argtransform_t argtransform, void *argtransform_data);
179 |
180 | // _nm_menu_inject handles the QMenu::aboutToShow signal and injects menu items.
181 | static void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at);
182 |
183 | static int nm_init() {
184 | #ifdef NM_UNINSTALL_CONFIGDIR
185 | NM_LOG("feature: NM_UNINSTALL_CONFIGDIR: true");
186 | #else
187 | NM_LOG("feature: NM_UNINSTALL_CONFIGDIR: false");
188 | #endif
189 |
190 | NM_LOG("updating config");
191 |
192 | int rev = nm_global_config_update();
193 | if (nm_err_peek())
194 | NM_LOG("... warning: error parsing config, will show a menu item with the error: %s", nm_err());
195 |
196 | size_t ntmp = SIZE_MAX;
197 | if (rev == -1) {
198 | NM_LOG("... info: no config file changes detected for initial config update (it should always return an error or update), stopping (this is a bug; err should have been returned instead)");
199 | } else if (!nm_global_config_items(&ntmp)) {
200 | NM_LOG("... warning: no menu items returned by nm_global_config_items, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)");
201 | } else if (ntmp == SIZE_MAX) {
202 | NM_LOG("... warning: no size returned by nm_global_config_items, ignoring for now (this is a bug)");
203 | } else if (!ntmp) {
204 | NM_LOG("... warning: size returned by nm_global_config_items is 0, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)");
205 | }
206 |
207 | return 0;
208 | }
209 |
210 | extern "C" __attribute__((visibility("default"))) MenuTextItem* _nm_menu_hook(void* _this, QMenu* menu, QString const& label, bool checkable, bool checked, QString const& thingy) {
211 | NM_LOG("AbstractNickelMenuController::createMenuTextItem(%p, `%s`, %d, %d, `%s`)", menu, qPrintable(label), checkable, checked, qPrintable(thingy));
212 |
213 | QString trmm = QCoreApplication::translate("StatusBarMenuController", "Settings");
214 | QString trrm = QCoreApplication::translate("DictionaryActionProxy", "Dictionary");
215 | QString trbm = QCoreApplication::translate("N3BrowserSettingsMenuController", "Keyboard");
216 | QString trlm = QCoreApplication::translate("LibraryViewMenuController", "Manage downloads");
217 | NM_LOG("Comparing against '%s', '%s', '%s', '%s'", qPrintable(trmm), qPrintable(trrm), qPrintable(trbm), qPrintable(trlm));
218 |
219 | nm_menu_location_t loc = {};
220 | if (label == trmm && !checkable) {
221 | NM_LOG("Intercepting main menu (label=Settings, checkable=false)...");
222 | loc = NM_MENU_LOCATION(main);
223 | } else if (label == trrm && !checkable) {
224 | NM_LOG("Intercepting reader menu (label=Dictionary, checkable=false)...");
225 | loc = NM_MENU_LOCATION(reader);
226 | } else if (label == trbm && !checkable) {
227 | NM_LOG("Intercepting browser menu (label=Keyboard, checkable=false)...");
228 | loc = NM_MENU_LOCATION(browser);
229 | } else if (label == trlm && !checkable) {
230 | NM_LOG("Intercepting library menu (label=Manage downloads, checkable=false)..."); // this is actually two menus: in "My Books", and in "My Articles"
231 | loc = NM_MENU_LOCATION(library);
232 | }
233 |
234 | if (loc)
235 | QObject::connect(menu, &QMenu::aboutToShow, std::bind(_nm_menu_inject, _this, menu, loc, menu->actions().count()));
236 |
237 | return AbstractNickelMenuController_createMenuTextItem(_this, menu, label, checkable, checked, thingy);
238 | }
239 |
240 | QString nm_menu_pixmap(const char *custom, const char *custom_temp_out, const char *fallback) {
241 | if (!custom || !custom_temp_out)
242 | return QString(fallback);
243 |
244 | QImage a;
245 | if (!a.load(QString(custom))) {
246 | NM_LOG("nm_menu_pixmap: error loading '%s', falling back to '%s': %s", custom, fallback, QFile::exists(custom) ? "failed to load image" : "image does not exist");
247 | return QString(fallback);
248 | }
249 |
250 | QPixmap b;
251 | if (!b.load(QString(fallback))) {
252 | NM_LOG("nm_menu_pixmap: error loading default pixmap '%s': %s", fallback, QFile::exists(fallback) ? "failed to load image" : "image does not exist");
253 | return QString(fallback);
254 | }
255 |
256 | QImage c = a.scaled(b.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
257 | NM_LOG("nm_menu_pixmap: resized '%s' to match '%s' (%dx%d): %dx%d", custom, fallback, b.size().width(), b.size().height(), c.size().width(), c.size().height());
258 |
259 | if (!c.save(QString(custom_temp_out), "PNG")) {
260 | NM_LOG("nm_menu_pixmap: error saving resized pixmap to '%s'", custom_temp_out);
261 | return QString(fallback);
262 | }
263 |
264 | return QString(custom_temp_out);
265 | }
266 |
267 | static const char *nm_main_menu_config(int index, const char *option) {
268 | char buf[strlen("menu_main_15505_9_icon_active") + 1];
269 | if (snprintf(buf, sizeof(buf), "menu_main_15505_%d_%s", index, option) >= static_cast(sizeof(buf))) {
270 | NM_LOG("Failed to create main menu config key for index %d, option %s", index, option);
271 | return nullptr;
272 | }
273 | return nm_global_config_experimental(buf);
274 | }
275 |
276 | static void main_nav_button_configure(MainNavButton *btn, const char *label, const char *icon, const char *icon_fallback, const char *icon_active, const char *icon_active_fallback) {
277 | if (label) {
278 | MainNavButton_setText(btn, label);
279 | }
280 |
281 | if (icon || icon_fallback) {
282 | QString pixmap = nm_menu_pixmap(
283 | icon,
284 | "/tmp/nm_menu.png",
285 | icon_fallback
286 | );
287 | if (!pixmap.isEmpty()) {
288 | MainNavButton_setPixmap(btn, pixmap);
289 | }
290 | }
291 |
292 | if (icon_active || icon_active_fallback) {
293 | QString pixmap = nm_menu_pixmap(
294 | icon_active,
295 | "/tmp/nm_menu.png",
296 | icon_active_fallback
297 | );
298 | if (!pixmap.isEmpty()) {
299 | MainNavButton_setActivePixmap(btn, pixmap);
300 | }
301 | }
302 |
303 | if (icon || icon_fallback || icon_active || icon_active_fallback) {
304 | QFile::remove("/tmp/nm_menu.png");
305 | }
306 | }
307 |
308 | extern "C" __attribute__((visibility("default"))) void _nm_menu_hook2(MainNavView *_this, QWidget *parent) {
309 | NM_LOG("MainNavView::MainNavView(%p, %p)", _this, parent);
310 | MainNavView_MainNavView(_this, parent);
311 |
312 | if (!MainNavButton_MainNavButton || !MainNavButton_setPixmap || !MainNavButton_setActivePixmap || !MainNavButton_setText || !MainNavButton_setText) {
313 | NM_LOG("Could not find required MainNavButton symbols, cannot add tab button for NickelMenu main menu.");
314 | return;
315 | }
316 |
317 | QHBoxLayout *bl = _this->findChild();
318 | if (!bl) {
319 | NM_LOG("Could not find QHBoxLayout(should contain MainNavButtons and be contained in the QVBoxLayout of MainNavView) in MainNavView, cannot add tab button for NickelMenu main menu.");
320 | return;
321 | }
322 |
323 | NM_LOG("Default main menu has %d buttons", bl->count());
324 | for (int i = 0; i < bl->count(); ++i) {
325 | QWidget *widget = bl->itemAt(i)->widget();
326 | NM_LOG("Main menu button %d = %s", i, widget ->objectName().toUtf8().constData());
327 |
328 | MainNavButton *btn = qobject_cast(widget);
329 | if (!btn) {
330 | NM_LOG("qobject_cast failed on button %d", i);
331 | continue;
332 | }
333 |
334 | const char *label = nm_main_menu_config(i, "label");
335 | const char *icon = nm_main_menu_config(i, "icon");
336 | const char *icon_active = nm_main_menu_config(i, "icon_active");
337 |
338 | const char *enabled = nm_main_menu_config(i, "enabled");
339 | main_nav_button_configure(btn, label, icon, nullptr, icon_active, nullptr);
340 | if (enabled) {
341 | if (strcmp("0", enabled) == 0) {
342 | NM_LOG("Main menu button %d disabled", i);
343 | widget->hide();
344 | } else if (strcmp("1", enabled) == 0) {
345 | NM_LOG("Main menu button %d explicitly enabled", i);
346 | widget->show();
347 | }
348 | }
349 | }
350 |
351 | const char *enabled = nm_global_config_experimental("menu_main_15505_enabled");
352 | if (enabled && strcmp("0", enabled) == 0) {
353 | NM_LOG("Main menu NickelMenu button disabled");
354 | return;
355 | }
356 |
357 | NM_LOG("Adding main menu button in tab bar for firmware 4.23.15505+.");
358 | MainNavButton *btn = reinterpret_cast(calloc(1, 256));
359 | if (!btn) { // way larger than a MainNavButton, but better to be safe
360 | NM_LOG("Failed to allocate memory for MainNavButton, cannot add tab button for NickelMenu main menu.");
361 | return;
362 | }
363 |
364 | MainNavButton_MainNavButton(btn, parent);
365 | main_nav_button_configure(btn,
366 | nm_global_config_experimental("menu_main_15505_label") ?: "NickelMenu",
367 | nm_global_config_experimental("menu_main_15505_icon"),
368 | ":/images/home/main_nav_more.png",
369 | nm_global_config_experimental("menu_main_15505_icon_active"),
370 | ":/images/home/main_nav_more_active.png"
371 | );
372 | btn->setObjectName("nmButton");
373 |
374 | QPushButton *sh = new QPushButton(_this); // HACK: we use a QPushButton as an adaptor so we can connect an old-style signal with the new-style connect without needing a custom QObject
375 | if (!QWidget::connect(btn, SIGNAL(tapped()), sh, SIGNAL(pressed()))) {
376 | NM_LOG("Failed to connect SIGNAL(tapped()) on TouchLabel to SIGNAL(pressed()) on the QPushButton shim, cannot add tab button for NickelMenu main menu.");
377 | return;
378 | }
379 | sh->setVisible(false);
380 |
381 | QWidget::connect(sh, &QPushButton::pressed, [btn] {
382 | if (!NickelTouchMenu_NickelTouchMenu || !MenuTextItem_MenuTextItem || !MenuTextItem_setText || !MenuTextItem_registerForTapGestures) {
383 | NM_LOG("could not find required NickelTouchMenu and MenuTextItem symbols for generating menu");
384 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Could not find required NickelTouchMenu and MenuTextItem symbols for generating menu (this is a bug)."));
385 | return;
386 | }
387 |
388 | NM_LOG("checking for config updates");
389 | int rev = nm_global_config_update();
390 | NM_LOG("revision = %d", rev);
391 |
392 | NM_LOG("building menu");
393 |
394 | size_t items_n;
395 | nm_menu_item_t **items = nm_global_config_items(&items_n);
396 |
397 | if (!items) {
398 | NM_LOG("failed to get menu items");
399 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Failed to get menu items (this might be a bug)."));
400 | return;
401 | }
402 |
403 | NickelTouchMenu *menu = reinterpret_cast(calloc(1, 512)); // about 3x larger than the largest menu I've seen in 15505 (most inherit from NickelTouchMenu) to be on the safe side
404 | if (!menu) {
405 | NM_LOG("failed to allocate memory for menu");
406 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Failed to allocate memory for menu."));
407 | return;
408 | }
409 |
410 | NickelTouchMenu_NickelTouchMenu(menu, nullptr, 3);
411 |
412 | for (size_t i = 0; i < items_n; i++) {
413 | nm_menu_item_t *it = items[i];
414 | if (it->loc != NM_MENU_LOCATION(main))
415 | continue;
416 |
417 | NM_LOG("adding item '%s'...", it->lbl);
418 |
419 | // based on _ZN23SelectionMenuController18createMenuTextItemEP7QWidgetRK7QString
420 | // (also see _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_, which seems to do the gestures itself instead of calling registerForTapGestures)
421 |
422 | MenuTextItem *mti = reinterpret_cast(calloc(1, 256)); // about 3x larger than the 15505 size (92)
423 | if (!it) {
424 | NM_LOG("failed to allocate memory for config item");
425 | menu->deleteLater();
426 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Failed to allocate memory for menu item."));
427 | return;
428 | }
429 |
430 | MenuTextItem_MenuTextItem(mti, menu, false, true);
431 | MenuTextItem_setText(mti, QString::fromUtf8(it->lbl));
432 | MenuTextItem_registerForTapGestures(mti); // this only makes the MenuTextItem::tapped signal connect so it highlights on tap, doesn't apply to the QAction::triggered below (which needs another GestureReceiver somewhere)
433 |
434 | // based on _ZN22AbstractMenuController12createActionEP5QMenuP7QWidgetbbb
435 |
436 | QWidgetAction *ac = new QWidgetAction(menu);
437 | ac->setDefaultWidget(mti);
438 | ac->setEnabled(true);
439 |
440 | menu->addAction(ac);
441 |
442 | QWidget::connect(ac, &QAction::triggered, menu, &QMenu::hide);
443 |
444 | if (i != items_n-1)
445 | menu->addSeparator();
446 |
447 | // shim so we don't need to deal with GestureReceiver directly like _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_ does
448 | // (similar to _ZN23SelectionMenuController11addMenuItemEP17SelectionMenuViewP12MenuTextItemPKc)
449 |
450 | if (!QWidget::connect(mti, SIGNAL(tapped(bool)), ac, SIGNAL(triggered()))) {
451 | NM_LOG("could not handle touch events for menu item (connection of SIGNAL(tapped(bool)) on MenuTextItem to SIGNAL(triggered()) on QWidgetAction failed)");
452 | menu->deleteLater();
453 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Could not attach touch event handlers to menu item (this is a bug)."));
454 | return;
455 | }
456 |
457 | // event handler
458 |
459 | QObject::connect(ac, &QAction::triggered, [it](bool) {
460 | NM_LOG("item '%s' pressed...", it->lbl);
461 | nm_menu_item_do(it, NULL, NULL);
462 | NM_LOG("done");
463 | }); // note: we're capturing by value, i.e. the pointer to the global variable, rather then the stack variable, so this is safe
464 | }
465 |
466 | NM_LOG("showing menu");
467 |
468 | QWidget::connect(menu, &QMenu::aboutToHide, menu, &QWidget::deleteLater);
469 |
470 | menu->ensurePolished();
471 | menu->popup(btn->mapToGlobal(btn->geometry().topRight() - QPoint(0, menu->sizeHint().height())));
472 | });
473 |
474 | bl->addWidget(btn, 1);
475 | _this->ensurePolished();
476 |
477 | NM_LOG("Added button.");
478 | }
479 |
480 | // _nm_menu_hook4_item gets/sets the current menu item. It must only be called
481 | // by one thread at a time. The item will be cleared after it has been used.
482 | nm_menu_item_t *_nm_menu_hook4_item(nm_menu_item_t *it) {
483 | static nm_menu_item_t *its = NULL;
484 | if (it)
485 | return (its = it);
486 | if (!its)
487 | return NULL;
488 | nm_menu_item_t *tmp = its;
489 | its = NULL;
490 | return tmp;
491 | }
492 |
493 | extern "C" __attribute__((visibility("default"))) void _nm_menu_hook3(SelectionMenuController *_this, SelectionMenuView* smv, MenuTextItem* mti, const char *slot) {
494 | NM_LOG("hook3: %p %p %p %s", _this, smv, mti, slot);
495 | SelectionMenuController_addMenuItem(_this, smv, mti, slot);
496 |
497 | if (!SelectionMenuView_addMenuItem || !SelectionMenuController_lookupWikipedia || !(SelectionMenuController_lookupWeb || (SelectionMenuController_lookupGoogle && SelectionMenuController_lookupBaidu))) {
498 | NM_LOG("could not find required SelectionMenuView and SelectionMenuController symbols for adding selection menu items");
499 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Could not find required SelectionMenuView and SelectionMenuController symbols for adding selection menu items (this is a bug)."));
500 | return;
501 | }
502 |
503 | // this is important for another reason other than positioning: it only displays if Volume::canSearch()
504 | nm_menu_location_t loc;
505 | if (!strcmp(slot, "1showSearchOptions()")) //libnickel 4.20.14622 * _ZN23SelectionMenuController17showSearchOptionsEv
506 | loc = NM_MENU_LOCATION(selection);
507 | else if (!strcmp(slot, "2lookupWeb()")) //libnickel 4.20.14622 4.30.18838 _ZN23SelectionMenuController9lookupWebEv
508 | loc = NM_MENU_LOCATION(selection_search);
509 | else if (!strcmp(slot, "2lookupGoogle()")) //libnickel 4.31.19086 * _ZN23SelectionMenuController12lookupGoogleEv
510 | loc = NM_MENU_LOCATION(selection_search);
511 | else if (!strcmp(slot, "2lookupBaidu()")) //libnickel 4.31.19086 * _ZN23SelectionMenuController11lookupBaiduEv
512 | loc = NM_MENU_LOCATION(selection_search);
513 | else
514 | return;
515 |
516 | NM_LOG("Found search item, injecting menu items after it.");
517 |
518 | NM_LOG("checking for config updates");
519 | int rev = nm_global_config_update();
520 | NM_LOG("revision = %d", rev);
521 |
522 | NM_LOG("adding items");
523 |
524 | size_t items_n;
525 | nm_menu_item_t **items = nm_global_config_items(&items_n);
526 |
527 | if (!items) {
528 | NM_LOG("failed to get menu items");
529 | ConfirmationDialogFactory_showOKDialog(QLatin1String("NickelMenu"), QLatin1String("Failed to get menu items (this might be a bug)."));
530 | return;
531 | }
532 |
533 | for (size_t i = 0; i < items_n; i++) {
534 | nm_menu_item_t *it = items[i];
535 | if (it->loc != loc)
536 | continue;
537 |
538 | NM_LOG("adding item '%s'...", it->lbl);
539 |
540 | // based on _ZN23SelectionMenuController18createMenuTextItemEP7QWidgetRK7QString
541 |
542 | MenuTextItem *mti = reinterpret_cast(calloc(1, 256)); // about 3x larger than the 15505 size (92)
543 | if (!it) {
544 | NM_LOG("failed to allocate memory for config item");
545 | return;
546 | }
547 |
548 | MenuTextItem_MenuTextItem(mti, smv, false, true);
549 | MenuTextItem_setText(mti, QString::fromUtf8(it->lbl));
550 | MenuTextItem_registerForTapGestures(mti);
551 |
552 | // based on _ZN23SelectionMenuController18setupSearchOptionsEb and _ZN23SelectionMenuController11addMenuItemEP17SelectionMenuViewP12MenuTextItemPKc, plus some custom stuff for better compatibility
553 |
554 | QPushButton *sh = new QPushButton(smv); // HACK: we use a QPushButton as an adaptor so we can connect an old-style signal with the new-style connect without needing a custom QObject
555 | if (!QWidget::connect(mti, SIGNAL(tapped(bool)), sh, SIGNAL(pressed()))) {
556 | NM_LOG("Failed to connect SIGNAL(tapped(bool)) on MenuTextItem to SIGNAL(pressed()) on the QPushButton shim, cannot add custom selection menu item.");
557 | return;
558 | }
559 | sh->setVisible(false);
560 |
561 | QObject::connect(sh, &QPushButton::pressed, [_this, it]() {
562 | NM_LOG("item '%s' pressed...", it->lbl);
563 | _nm_menu_hook4_item(it); // this is safe since it is a pointer captured by value
564 | NM_LOG("triggering lookupWikipedia() slot");
565 | SelectionMenuController_lookupWikipedia(_this);
566 | });
567 |
568 | SelectionMenuView_addMenuItem(smv, mti);
569 | }
570 | }
571 |
572 | typedef struct {
573 | QString const& selection;
574 | } nm_selmenu_argtransform_data_t;
575 |
576 | char *_nm_selmenu_argtransform(void *data, const char *arg) {
577 | nm_selmenu_argtransform_data_t *d = (nm_selmenu_argtransform_data_t*)(data);
578 |
579 | QString src = QString::fromUtf8(arg), res;
580 | QRegularExpression re = QRegularExpression("\\{([1])\\|([aAfnsSuwx]*)\\|([\"$%]*)\\}");
581 |
582 | for (QStringRef x = src.midRef(0); x.length() > 0;) {
583 | QRegularExpressionMatch m = re.match(x.toString());
584 |
585 | if (!m.hasMatch()) {
586 | res += x;
587 | x = x.mid(x.length());
588 | continue;
589 | }
590 |
591 | QString tmp;
592 |
593 | for (int k = 0; k < m.capturedLength(1); k++) {
594 | switch (m.capturedRef(1).at(k).toLatin1()) {
595 | case '1': tmp = d->selection; break;
596 | }
597 | }
598 |
599 | for (int k = 0; k < m.capturedLength(2); k++) {
600 | switch (m.capturedRef(2).at(k).toLatin1()) {
601 | case 'a': tmp = tmp.toLower(); break;
602 | case 'A': tmp = tmp.toUpper(); break;
603 | case 'f': tmp = tmp.split(QRegularExpression("\\s")).first(); break;
604 | case 'n': tmp = tmp.remove(QRegularExpression("[^0-9a-zA-Z]")); break;
605 | case 's': tmp = tmp.trimmed(); break;
606 | case 'S': tmp = tmp.simplified(); break;
607 | case 'u': if (tmp.length() == 0) { nm_err_set("argtransform: empty substitution result for %s", qPrintable(m.capturedRef(0).toString())); return NULL; }; break;
608 | case 'w': tmp = tmp.remove(QRegularExpression("\\s")); break;
609 | case 'x': tmp = tmp.replace(QRegularExpression("\\s"), "_"); break;
610 | }
611 | }
612 |
613 | for (int k = 0; k < m.capturedLength(3); k++) {
614 | switch (m.capturedRef(3).at(k).toLatin1()) {
615 | case '"':
616 | tmp = tmp
617 | .replace("\"", "\\\"")
618 | .replace("\n", "\\n")
619 | .replace("\b", "\\b")
620 | .replace("\t", "\\t")
621 | .replace("\f", "\\f")
622 | .replace("\r", "\\r")
623 | .replace("\\", "\\\\");
624 | break;
625 | case '$':
626 | tmp = tmp.replace("'", "'\"'\"'");
627 | break;
628 | case '%':
629 | tmp = QUrl::toPercentEncoding(tmp);
630 | break;
631 | }
632 | }
633 |
634 | res += x.left(m.capturedStart());
635 | res += tmp;
636 | x = x.mid(m.capturedEnd());
637 | }
638 |
639 | char *x = strdup(res.toUtf8().data());
640 | if (!x)
641 | nm_err_set("argtransform: could not allocate memory: %m");
642 | return x;
643 | }
644 |
645 | extern "C" __attribute__((visibility("default"))) void _nm_menu_hook4(WebSearchMixinBase *_this, QString const& selection, QString const& locale) {
646 | NM_LOG("hook4: %p %s %s", _this, qPrintable(selection), qPrintable(locale));
647 |
648 | nm_menu_item_t *it = _nm_menu_hook4_item(NULL);
649 | if (!it) {
650 | NM_LOG("No current menu item, continuing with default wikipedia search.");
651 | WebSearchMixinBase_doWikipediaSearch(_this, selection, locale);
652 | return;
653 | }
654 |
655 | NM_LOG("continuing execution of item %p (%s)", it, it->lbl);
656 | nm_selmenu_argtransform_data_t data = (nm_selmenu_argtransform_data_t){
657 | .selection = selection,
658 | };
659 | nm_menu_item_do(it, _nm_selmenu_argtransform, (void*)(&data)); // this is safe since data will not be used after this returns
660 | NM_LOG("done");
661 | }
662 |
663 | void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at) {
664 | NM_LOG("inject %d @ %d", loc, at);
665 |
666 | int rev_o = menu->property("nm_config_rev").toInt();
667 |
668 | NM_LOG("checking for config updates (current revision: %d)", rev_o);
669 | int rev_n = nm_global_config_update(); // if there was an error it will be returned as a menu item anyways (and updated will be true)
670 | NM_LOG("new revision = %d%s", rev_n, rev_n == rev_o ? "" : " (changed)");
671 |
672 | NM_LOG("checking for existing items added by nm");
673 |
674 | for (auto action : menu->actions()) {
675 | if (action->property("nm_action") == true) {
676 | if (rev_o == rev_n)
677 | return; // already added items, menu is up to date
678 | menu->removeAction(action);
679 | delete action;
680 | }
681 | }
682 |
683 | NM_LOG("getting insertion point");
684 |
685 | auto actions = menu->actions();
686 | auto before = at < actions.count()
687 | ? actions.at(at)
688 | : nullptr;
689 |
690 | if (before == nullptr)
691 | NM_LOG("it seems the original item to add new ones before was never actually added to the menu (number of items when the action was created is %d, current is %d), appending to end instead", at, actions.count());
692 |
693 | NM_LOG("injecting new items");
694 |
695 | size_t items_n;
696 | nm_menu_item_t **items = nm_global_config_items(&items_n);
697 |
698 | if (!items) {
699 | NM_LOG("items is NULL (either the config hasn't been parsed yet or there was a memory allocation error), not adding");
700 | return;
701 | }
702 |
703 | // if it segfaults in createMenuTextItem, it's likely because
704 | // AbstractNickelMenuController is invalid, which shouldn't happen while the
705 | // menu which we added the signal from still can be shown... (but
706 | // theoretically, it's possible)
707 |
708 | for (size_t i = 0; i < items_n; i++) {
709 | nm_menu_item_t *it = items[i];
710 | if (it->loc != loc)
711 | continue;
712 |
713 | NM_LOG("adding item '%s'...", it->lbl);
714 |
715 | MenuTextItem* item = AbstractNickelMenuController_createMenuTextItem(nmc, menu, QString::fromUtf8(it->lbl), false, false, "");
716 | QAction* action = AbstractNickelMenuController_createAction_before(before, loc, i == items_n-1, nmc, menu, item, true, true, true);
717 |
718 | QObject::connect(action, &QAction::triggered, [it](bool){
719 | NM_LOG("item '%s' pressed...", it->lbl);
720 | nm_menu_item_do(it, NULL, NULL);
721 | NM_LOG("done");
722 | }); // note: we're capturing by value, i.e. the pointer to the global variable, rather then the stack variable, so this is safe
723 | }
724 |
725 | NM_LOG("updating config revision property");
726 | menu->setProperty("nm_config_rev", rev_n);
727 | }
728 |
729 | void nm_menu_item_do(nm_menu_item_t *it, nm_argtransform_t argtransform, void *argtransform_data) {
730 | const char *err = NULL;
731 | bool success = true;
732 | int skip = 0;
733 |
734 | for (nm_menu_action_t *cur = it->action; cur; cur = cur->next) {
735 | NM_LOG("action %p with argument %s : ", cur->act, cur->arg);
736 | NM_LOG("...success=%d ; on_success=%d on_failure=%d skip=%d", success, cur->on_success, cur->on_failure, skip);
737 |
738 | if (skip != 0) {
739 | NM_LOG("...skipping action due to skip flag (remaining=%d)", skip);
740 | if (skip > 0)
741 | skip--;
742 | continue;
743 | } else if (!((success && cur->on_success) || (!success && cur->on_failure))) {
744 | NM_LOG("...skipping action due to condition flags");
745 | continue;
746 | }
747 |
748 | nm_action_result_t *res = NULL;
749 | if (!argtransform) {
750 | res = cur->act(cur->arg);
751 | } else {
752 | NM_LOG("...applying argtransform");
753 | char *arg = (*argtransform)(argtransform_data, cur->arg);
754 | if (arg) {
755 | NM_LOG("...applied argtransform: %s", arg);
756 | res = cur->act(arg);
757 | free(arg);
758 | }
759 | }
760 | err = nm_err();
761 |
762 | if (err == NULL && res && res->type == NM_ACTION_RESULT_TYPE_SKIP) {
763 | NM_LOG("...not updating success flag (value=%d) for skip result", success);
764 | } else if (!(success = err == NULL)) {
765 | NM_LOG("...error: '%s'", err);
766 | continue;
767 | } else if (!res) {
768 | NM_LOG("...warning: you should have returned a result with type silent, not null, upon success");
769 | continue;
770 | }
771 |
772 | NM_LOG("...result: type=%d msg='%s', handling...", res->type, res->msg);
773 |
774 | MainWindowController *mwc;
775 | switch (res->type) {
776 | case NM_ACTION_RESULT_TYPE_SILENT:
777 | break;
778 | case NM_ACTION_RESULT_TYPE_MSG:
779 | ConfirmationDialogFactory_showOKDialog(QString::fromUtf8(it->lbl), QString::fromUtf8(res->msg));
780 | break;
781 | case NM_ACTION_RESULT_TYPE_TOAST:
782 | mwc = MainWindowController_sharedInstance();
783 | if (!mwc) {
784 | NM_LOG("toast: could not get shared main window controller pointer");
785 | break;
786 | }
787 | MainWindowController_toast(mwc, QString::fromUtf8(res->msg), QStringLiteral(""), 1500);
788 | break;
789 | case NM_ACTION_RESULT_TYPE_SKIP:
790 | skip = res->skip;
791 | break;
792 | }
793 |
794 | if (skip == -1)
795 | NM_LOG("...skipping remaining actions");
796 | else if (skip != 0)
797 | NM_LOG("...skipping next %d actions", skip);
798 |
799 | nm_action_result_free(res);
800 | }
801 |
802 | if (err) {
803 | NM_LOG("last action returned error %s", err);
804 | ConfirmationDialogFactory_showOKDialog(QString::fromUtf8(it->lbl), QString::fromUtf8(err));
805 | }
806 | }
807 |
808 | QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_menu_location_t loc, bool last_in_group, void *_this, QMenu *menu, QWidget *widget, bool close, bool enabled, bool separator) {
809 | int n = menu->actions().count();
810 | QAction* action = AbstractNickelMenuController_createAction(_this, menu, widget, /*close*/false, enabled, /*separator*/false);
811 |
812 | action->setProperty("nm_action", true);
813 |
814 | if (!menu->actions().contains(action)) {
815 | NM_LOG("could not find added action at end of menu (note: old count is %d, new is %d), not moving it to the right spot or adding separator", n, menu->actions().count());
816 | return action;
817 | }
818 |
819 | if (before != nullptr) {
820 | menu->removeAction(action);
821 | menu->insertAction(before, action);
822 | }
823 |
824 | if (close) {
825 | // we can't use the signal which createAction can create, as it gets lost when moving the item
826 | QWidget::connect(action, &QAction::triggered, [=](bool){ menu->hide(); });
827 | }
828 |
829 | if (separator) {
830 | // if it's the main menu, we generally want to use a custom separator
831 | QAction *sep;
832 | if (loc == NM_MENU_LOCATION(main) && LightMenuSeparator_LightMenuSeparator && BoldMenuSeparator_BoldMenuSeparator) {
833 | sep = reinterpret_cast(calloc(1, 32)); // it's actually 8 as of 14622, but better to be safe
834 | (last_in_group
835 | ? BoldMenuSeparator_BoldMenuSeparator
836 | : LightMenuSeparator_LightMenuSeparator
837 | )(sep, reinterpret_cast(_this));
838 | menu->insertAction(before, sep);
839 | } else {
840 | sep = menu->insertSeparator(before);
841 | }
842 | sep->setProperty("nm_action", true);
843 | }
844 |
845 | return action;
846 | }
847 |
848 |
--------------------------------------------------------------------------------
/src/nickelmenu.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_H
2 | #define NM_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | #include
8 | #include "action.h"
9 |
10 | #define NM_MENU_LOCATION(name) NM_MENU_LOCATION_##name
11 |
12 | #define NM_MENU_LOCATIONS \
13 | X(main) \
14 | X(reader) \
15 | X(browser) \
16 | X(library) \
17 | X(selection) \
18 | X(selection_search)
19 |
20 | typedef enum {
21 | NM_MENU_LOCATION_NONE = 0, // to allow it to be checked with if
22 | #define X(name) \
23 | NM_MENU_LOCATION(name),
24 | NM_MENU_LOCATIONS
25 | #undef X
26 | } nm_menu_location_t;
27 |
28 | typedef struct nm_menu_action_t {
29 | char *arg;
30 | bool on_success;
31 | bool on_failure;
32 | nm_action_fn_t act; // can block, must return zero on success, nonzero with nm_err set on error
33 | struct nm_menu_action_t *next;
34 | } nm_menu_action_t;
35 |
36 | typedef struct {
37 | nm_menu_location_t loc;
38 | char *lbl;
39 | nm_menu_action_t *action;
40 | } nm_menu_item_t;
41 |
42 | #ifdef __cplusplus
43 | }
44 | #endif
45 | #endif
46 |
--------------------------------------------------------------------------------
/src/util.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | static __thread bool nm_err_state = false;
7 | static __thread char nm_err_buf[2048] = {0};
8 | static __thread char nm_err_buf_tmp[sizeof(nm_err_buf)] = {0}; // in case the format string overlaps
9 |
10 | const char *nm_err() {
11 | if (nm_err_state) {
12 | nm_err_state = false;
13 | return nm_err_buf;
14 | }
15 | return NULL;
16 | }
17 |
18 | const char *nm_err_peek() {
19 | if (nm_err_state)
20 | return nm_err_buf;
21 | return NULL;
22 | }
23 |
24 | bool nm_err_set(const char *fmt, ...) {
25 | va_list a;
26 | if ((nm_err_state = !!fmt)) {
27 | va_start(a, fmt);
28 | int r = vsnprintf(nm_err_buf_tmp, sizeof(nm_err_buf_tmp), fmt, a);
29 | if (r < 0)
30 | r = snprintf(nm_err_buf_tmp, sizeof(nm_err_buf_tmp), "error applying format to error string '%s'", fmt);
31 | if (r >= (int)(sizeof(nm_err_buf_tmp))) {
32 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 2] = '.';
33 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 3] = '.';
34 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 4] = '.';
35 | }
36 | memcpy(nm_err_buf, nm_err_buf_tmp, sizeof(nm_err_buf));
37 | va_end(a);
38 | }
39 | return nm_err_state;
40 | }
41 |
--------------------------------------------------------------------------------
/src/util.h:
--------------------------------------------------------------------------------
1 | #ifndef NM_UTIL_H
2 | #define NM_UTIL_H
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 |
14 | // strtrim trims ASCII whitespace in-place (i.e. don't give it a string literal)
15 | // from the left/right of the string.
16 | __attribute__((unused)) static inline char *strtrim(char *s) {
17 | if (!s) return NULL;
18 | char *a = s, *b = s + strlen(s);
19 | for (; a < b && isspace((unsigned char)(*a)); a++);
20 | for (; b > a && isspace((unsigned char)(*(b-1))); b--);
21 | *b = '\0';
22 | return a;
23 | }
24 |
25 | // NM_LOG writes a log message.
26 | #define NM_LOG(fmt, ...) nh_log(fmt " (%s:%d)", ##__VA_ARGS__, __FILE__, __LINE__)
27 |
28 | // Error handling (thread-safe):
29 |
30 | // nm_err returns the current error message and clears the error state. If there
31 | // isn't any error set, NULL is returned. The returned string is only valid on
32 | // the current thread until nm_err_set is called.
33 | const char *nm_err();
34 |
35 | // nm_err_peek is like nm_err, but doesn't clear the error state.
36 | const char *nm_err_peek();
37 |
38 | // nm_err_set sets the current error message to the specified format string. If
39 | // fmt is NULL, the error is cleared. It is safe to use the return value of
40 | // nm_err as an argument. If fmt was not NULL, true is returned.
41 | bool nm_err_set(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
42 |
43 | // NM_ERR_SET set is like nm_err_set, but also includes information about the
44 | // current file/line. To set it to NULL, use nm_err_set directly.
45 | #define NM_ERR_SET(fmt, ...) \
46 | nm_err_set((fmt " (%s:%d)"), ##__VA_ARGS__, __FILE__, __LINE__);
47 |
48 | // NM_ERR_RET is like NM_ERR_SET, but also returns the specified value.
49 | #define NM_ERR_RET(ret, fmt, ...) do { \
50 | NM_ERR_SET(fmt, ##__VA_ARGS__); \
51 | return (ret); \
52 | } while (0)
53 |
54 | // NM_CHECK checks a condition and calls nm_err_set then returns the specified
55 | // value if the condition is false. Otherwise, nothing happens.
56 | #define NM_CHECK(ret, cond, fmt, ...) do { \
57 | if (!(cond)) { \
58 | nm_err_set((fmt " (check failed: %s)"), ##__VA_ARGS__, #cond); \
59 | return (ret); \
60 | } \
61 | } while (0)
62 |
63 | #ifdef __cplusplus
64 | }
65 | #endif
66 | #endif
67 |
--------------------------------------------------------------------------------
/test/syms/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/pgaskin/NickelMenu/test/syms
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/pgaskin/kobopatch v0.15.1
7 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8
8 | )
9 |
--------------------------------------------------------------------------------
/test/syms/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
4 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
5 | github.com/pgaskin/czlib v0.0.4 h1:biwyjtvo6xiXwvgYWyKz0GpmAmDJi4as3zl8Go7Pr9w=
6 | github.com/pgaskin/czlib v0.0.4/go.mod h1:ZRHNrWwa4Jv0HU5r0u64eKRZXcBUicpI6rtaEEbduaU=
7 | github.com/pgaskin/kobopatch v0.15.1 h1:UjuC5sTs0LLFdMGHL24V7OtxIoyw1INHoJlyUtfFBAs=
8 | github.com/pgaskin/kobopatch v0.15.1/go.mod h1:GKP38Tq6b0oA2YaBVCZqP9qZjnbq+O9+w60VapmUrl4=
9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89 h1:hMsoSMebpfpaDW7+B7gsxNnMBNChjekeqmK8wkzAlc0=
11 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89/go.mod h1:yc5MYwuNUGggTQ8++IDAbOYq/9PXxsg73+EHYgoG/4w=
12 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
15 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
16 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
17 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
18 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
19 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
22 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064 h1:bBbas3KhLwE6f59Z9lUipY23xUX9qrvyLBdQzzV2Tko=
24 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064/go.mod h1:MVYPdlFruujBlzEY3x2Q3XBk7XLdYRNZ7zDbrzYFO7w=
25 |
--------------------------------------------------------------------------------
/test/syms/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/tar"
5 | "bufio"
6 | "bytes"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "net/http"
11 | "os"
12 | "path/filepath"
13 | "sort"
14 | "strconv"
15 | "strings"
16 |
17 | "github.com/pgaskin/kobopatch/patchlib"
18 | "github.com/xi2/xz"
19 | )
20 |
21 | func main() {
22 | sc, err := FindSymChecks(".")
23 | if err != nil {
24 | fmt.Fprintf(os.Stderr, "[FTL] find symbol checks: %v\n", err)
25 | os.Exit(1)
26 | return
27 | }
28 |
29 | versions := []string{
30 | "4.6.9960", "4.6.9995", "4.7.10075", "4.7.10364", "4.7.10413",
31 | "4.8.10956", "4.8.11073", "4.8.11090", "4.9.11311", "4.9.11314",
32 | "4.10.11591", "4.10.11655", "4.11.11911", "4.11.11976", "4.11.11980",
33 | "4.11.11982", "4.11.12019", "4.12.12111", "4.13.12638", "4.14.12777",
34 | "4.15.12920", "4.16.13162", "4.17.13651", "4.17.13694", "4.18.13737",
35 | "4.19.14123", "4.20.14601", "4.20.14617", "4.20.14622", "4.21.15015",
36 | "4.22.15190", "4.22.15268", "4.23.15505", "4.24.15672", "4.24.15676",
37 | "4.25.15875", "4.26.16704", "4.28.17623", "4.28.17820", "4.28.17826",
38 | "4.28.17925", "4.28.18220", "4.29.18730", "4.30.18838", "4.31.19086",
39 | "4.32.19501", "4.33.19608", "4.33.19611", "4.33.19759", "4.34.20097",
40 | "4.35.20400", "4.36.21095",
41 | }
42 |
43 | checks := map[string]map[string][]SymCheck{}
44 | for _, c := range sc {
45 | var sm, em int
46 | for _, version := range versions {
47 | if c.StartVersion == "*" || strings.HasPrefix(version+".", c.StartVersion+".") {
48 | sm++
49 | }
50 | if c.EndVersion == "*" || strings.HasPrefix(version+".", c.EndVersion+".") {
51 | em++
52 | }
53 | if versioncmp(c.StartVersion, version) <= 0 && versioncmp(version, c.EndVersion) <= 0 {
54 | if _, ok := checks[version]; !ok {
55 | checks[version] = map[string][]SymCheck{}
56 | }
57 | checks[version][c.Library] = append(checks[version][c.Library], c)
58 | }
59 | }
60 | if sm == 0 {
61 | fmt.Printf("[WRN] %s: no exact match for the base version in specifier %#v\n", c.File, c.StartVersion)
62 | }
63 | if em == 0 {
64 | fmt.Printf("[WRN] %s: no exact match for the base version in specifier %#v\n", c.File, c.EndVersion)
65 | }
66 | }
67 |
68 | var checkVersions []string
69 | for version := range checks {
70 | checkVersions = append(checkVersions, version)
71 | }
72 | sort.Slice(checkVersions, func(i, j int) bool {
73 | return versioncmp(checkVersions[i], checkVersions[j]) == -1
74 | })
75 |
76 | var errs []error
77 | gherrs := map[string][]string{}
78 | for _, version := range checkVersions {
79 | var checkLibs []string
80 | for lib := range checks[version] {
81 | checkLibs = append(checkLibs, lib)
82 | }
83 | sort.Strings(checkLibs)
84 |
85 | for _, lib := range checkLibs {
86 | fmt.Printf("[INF] checking %s@%s\n", lib, version)
87 |
88 | pt, err := GetPatcher(version, lib)
89 | if err != nil {
90 | fmt.Fprintf(os.Stderr, "[FTL] get patcher: %v\n", err)
91 | os.Exit(1)
92 | return
93 | } else if pt == nil {
94 | fmt.Printf("[WRN] no data available, skipping\n")
95 | continue
96 | }
97 |
98 | _, err = pt.ExtractDynsyms(true)
99 | if err != nil {
100 | fmt.Fprintf(os.Stderr, "[FTL] extract symbols: %v\n", err)
101 | os.Exit(1)
102 | return
103 | }
104 |
105 | for _, check := range checks[version][lib] {
106 | fmt.Printf("[INF] %s:\n checking for one of %+s\n", check.File, check.Symbols)
107 | var f bool
108 | for _, sym := range check.Symbols {
109 | off, err := pt.ResolveSym(sym)
110 | if err != nil {
111 | fmt.Printf(" %s not found\n", sym)
112 | } else {
113 | fmt.Printf(" %s found at %#x\n", sym, off)
114 | f = true
115 | }
116 | }
117 | if !f {
118 | err := fmt.Errorf("%s: one of %+s not found in %s@%s", check.File, check.Symbols, lib, version)
119 | fmt.Printf("[ERR] %v\n", err)
120 | errs = append(errs, err)
121 |
122 | spl := strings.Split(check.File, ":")
123 | gherrf := fmt.Sprintf("file=%s,line=%s,col=%s", spl[0], spl[1], spl[2])
124 | gherrs[gherrf] = append(gherrs[gherrf], fmt.Sprintf("one of symbols %+s not found in %s@%s", check.Symbols, lib, version))
125 | }
126 | }
127 | }
128 | }
129 | if len(errs) == 0 {
130 | os.Exit(0)
131 | }
132 |
133 | fmt.Printf("[FTL] check failed\n")
134 | for _, err := range errs {
135 | fmt.Printf(" %v\n", err)
136 | }
137 | if os.Getenv("GITHUB_ACTIONS") == "true" {
138 | var ghfs []string
139 | for ghf := range gherrs {
140 | ghfs = append(ghfs, ghf)
141 | }
142 | sort.Strings(ghfs)
143 | for _, ghf := range ghfs {
144 | fmt.Printf("::error %s::%s\n", ghf, strings.Join(gherrs[ghf], "%0A"))
145 | }
146 | }
147 | os.Exit(1)
148 | }
149 |
150 | func GetPatcher(version, lib string) (*patchlib.Patcher, error) {
151 | resp, err := http.Get("https://github.com/pgaskin/kobopatch-testdata/raw/v1/" + version + ".tar.xz")
152 | if err != nil {
153 | return nil, fmt.Errorf("get kobopatch testdata for %#v: %w", version, err)
154 | }
155 | defer resp.Body.Close()
156 |
157 | if resp.StatusCode == http.StatusNotFound {
158 | return nil, nil
159 | } else if resp.StatusCode != http.StatusOK {
160 | return nil, fmt.Errorf("get kobopatch testdata for %#v: response status %s", version, resp.Status)
161 | }
162 |
163 | zr, err := xz.NewReader(resp.Body, 0)
164 | if err != nil {
165 | return nil, fmt.Errorf("read kobopatch testdata: %w", err)
166 | }
167 |
168 | tr := tar.NewReader(zr)
169 | for {
170 | th, err := tr.Next()
171 | if err != nil {
172 | if err == io.EOF {
173 | return nil, fmt.Errorf("read kobopatch testdata: file %#v not found", lib)
174 | }
175 | return nil, fmt.Errorf("read kobopatch testdata: %w", err)
176 | }
177 | if filepath.Clean(th.Name) == filepath.Clean(lib) {
178 | break
179 | }
180 | }
181 |
182 | buf, err := ioutil.ReadAll(tr)
183 | if err != nil {
184 | return nil, fmt.Errorf("read kobopatch testdata: %w", err)
185 | }
186 | return patchlib.NewPatcher(buf), nil
187 | }
188 |
189 | type SymCheck struct {
190 | File string
191 | Library string
192 | StartVersion string
193 | EndVersion string
194 | Symbols []string // or
195 | }
196 |
197 | func FindSymChecks(dir string) ([]SymCheck, error) {
198 | var checks []SymCheck
199 | if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
200 | var m bool
201 | for _, ext := range []string{".c", ".cc", ".cpp", ".h"} {
202 | if filepath.Ext(path) == ext {
203 | m = true
204 | break
205 | }
206 | }
207 | if !m {
208 | return nil
209 | }
210 |
211 | f, err := os.OpenFile(path, os.O_RDONLY, 0)
212 | if err != nil {
213 | return fmt.Errorf("open %#v: %w", path, err)
214 | }
215 | defer f.Close()
216 |
217 | sc := bufio.NewScanner(f)
218 | var line int
219 | for sc.Scan() {
220 | line++
221 | col := bytes.Index(sc.Bytes(), []byte("//libnickel"))
222 | if col == -1 {
223 | continue
224 | }
225 |
226 | args := strings.Fields(string(bytes.TrimSpace(sc.Bytes()[col+len("//libnickel"):])))
227 | if len(args) < 3 || args[0] == "*" {
228 | return fmt.Errorf("parse %#v: line %d, col %d: expected comment to be in the format '//libnickel ...'", path, line, col+1)
229 | }
230 |
231 | checks = append(checks, SymCheck{
232 | File: fmt.Sprintf("%s:%d:%d", path, line, col+1),
233 | Library: "libnickel.so.1.0.0",
234 | StartVersion: args[0],
235 | EndVersion: args[1],
236 | Symbols: args[2:],
237 | })
238 | }
239 | if err := sc.Err(); err != nil {
240 | return fmt.Errorf("read %#v: %w", path, err)
241 | }
242 |
243 | return nil
244 | }); err != nil {
245 | return nil, err
246 | }
247 | return checks, nil
248 | }
249 |
250 | func versioncmp(a, b string) int {
251 | if a == "*" || b == "*" {
252 | return 0
253 | }
254 | aspl, bspl := splint(a), splint(b)
255 | mlen := len(aspl)
256 | if len(bspl) > mlen {
257 | mlen = len(bspl)
258 | }
259 | for i := 0; i < mlen; i++ {
260 | switch {
261 | case i == len(bspl):
262 | return 1
263 | case i == len(aspl):
264 | return -1
265 | case aspl[i] > bspl[i]:
266 | return 1
267 | case bspl[i] > aspl[i]:
268 | return -1
269 | }
270 | }
271 | return 0
272 | }
273 |
274 | func splint(str string) []int64 {
275 | spl := strings.Split(str, ".")
276 | ints := make([]int64, len(spl))
277 | for i, p := range spl {
278 | ints[i], _ = strconv.ParseInt(p, 10, 64)
279 | }
280 | return ints
281 | }
282 |
--------------------------------------------------------------------------------