Compare commits

...
Author SHA1 Message Date
deepend c630730a1d negotiate explicitly registered IRC capabilities 2026-10-02 14:15:22 -06:00
deepend 9126ce9202 filter hooks by connection and context in core 2026-10-02 14:14:54 -06:00
deepend d3f892e9a7 add opt-in post-display print hooks 2026-10-02 14:14:24 -06:00
deepend bf89691b3f add connection-scoped context navigation 2026-10-02 14:13:59 -06:00
deepend 5029a34fcb add explicit IRC-aware comparison keys 2026-10-02 14:13:30 -06:00
deepend 73ccded3ac add opt-in asyncio task integration 2026-10-02 14:13:00 -06:00
deepend 44e677a973 add structured IRC event hooks 2026-10-02 14:12:37 -06:00
deepend 92d39cd077 expose main-loop file descriptor hooks 2026-10-02 14:11:40 -06:00
deepend 1cdee8ea6b reserve additive API slots and load optional Python modules 2026-10-02 14:11:14 -06:00
deepend-tildeclub e11604fd62 Merge pull request #405 from ZoiteChat/fix-dialog-layout
make dialog content fill available space
2026-10-01 05:23:22 -07:00
deepend a2e8d9087e make dialog content fill available space 2026-09-30 16:22:47 -06:00
deepend-tildeclub edf268dc0f Merge pull request #404 from ZoiteChat/run-as-root
Block root unless --run-as-root
2026-09-30 10:23:51 -07:00
deepend e03ea59f3c Block root unless --run-as-root 2026-09-30 10:42:28 -06:00
deepend f0b2840668 ensure fixes added to main site are reflected on docs site 2026-09-29 15:33:41 -06:00
deepend-tildeclub 4f2fc49475 Merge pull request #402 from ZoiteChat/docs-theme-fixes
docs site theme consistency fixes
2026-09-29 08:51:46 -07:00
deepend 2e4155bbfc docs site theme consistency fixes 2026-09-29 09:50:22 -06:00
deepend-tildeclub ca2456cbc6 Merge pull request #401 from ZoiteChat/fix-ircv3-typing
Fix IRCv3 typing notifications
2026-09-26 05:52:36 -07:00
deepend 222b4868f5 Fix IRCv3 typing notifications 2026-09-26 06:10:20 -06:00
deepend-tildeclub 2cf1f7e833 Merge pull request #398 from ZoiteChat/fix-issue-361
restrict input redraws to the entry widget
2026-09-19 22:33:03 -07:00
deepend 776d24695e keep X11 selection ownership off the chat window 2026-09-18 05:29:03 -06:00
deepend-tildeclub 64a9c6e5d7 Merge pull request #397 from ZoiteChat/fix-issue-392
Fix Wayland text selection crash
2026-09-17 10:14:14 -07:00
deepend 66196013bc Fix Wayland text selection crash 2026-09-16 16:23:38 -06:00
deepend-tildeclub 61ff308c9f Merge pull request #395 from B4reMetal/fix-userlist-color-pref
fix(userlist): decouple userlist nick colors from text_color_nicks
2026-09-16 14:00:16 -07:00
deepend 2672f9b087 Start acknowleding 2.20 development 2026-09-16 01:16:35 -06:00
deepend 4315a75a12 add 2.19.1 release notes 2026-09-16 00:10:49 -06:00
BareMetal 15905bebb1 fix(userlist): decouple userlist nick colors from text_color_nicks 2026-09-15 19:38:34 -06:00
deepend-tildeclub fc4a7454ad Merge pull request #391 from ZoiteChat/fix-away-tracking
Fix away tracking
2026-09-15 11:45:16 -07:00
deepend 87410aea50 Fix away tracking 2026-09-15 12:30:04 -06:00
deepend-tildeclub de98d00a1c Merge pull request #390 from ZoiteChat/fix-issue-369
defer channel tree scrolling until realized
2026-09-14 05:36:00 -07:00
deepend e94ffd9d79 defer channel tree scrolling until realized 2026-09-14 04:11:23 -06:00
deepend-tildeclub d7aebb784d Merge pull request #387 from ZoiteChat/tab-font-custom
add custom tab fonts and reduce close button size
2026-09-14 02:56:19 -07:00
deepend-tildeclub 20b8cb939d Merge pull request #389 from ZoiteChat/fix-issue-386
fix theme service tests
2026-09-14 02:47:50 -07:00
deepend ff4217c3d1 fix theme service tests 2026-09-13 15:34:56 -06:00
deepend 4fb72d30d8 add custom tab fonts and reduce close button size 2026-09-12 06:43:08 -06:00
deepend-tildeclub d2b8fc0bda Merge pull request #352 from ZoiteChat/inline-images
support inline images in chat.
2026-09-11 02:51:36 -07:00
deepend 951181628a Bundle GIO TLS support in AppImage 2026-09-11 03:30:43 -06:00
deepend-tildeclub ccee274847 Merge pull request #384 from salahcoronya/build-improvements
Fix build bug and improve build process
2026-09-11 02:13:17 -07:00
deepend 967f6728ef Use OpenSSL for Win HTTPS image downloads 2026-09-11 02:58:53 -06:00
deepend-tildeclub bf11ffb626 Merge pull request #379 from ZoiteChat/fix-issue-373
Add formatting status bar to input box
2026-09-11 01:14:09 -07:00
deepend c12cabbe4e Add formatting status bar to input box 2026-09-10 22:40:11 -06:00
Christopher Byrne 040021ee0e buuld: Fix deprecated features since 0.56
Now the the minimum is 0.59, silence some warning in Meson:

dependency.get_pkgconfig_variable becomes
depenency.get_variable(pkgconfig: 'var_name')
executable(gui_app = true) becomes
executable(win_subsystem = 'windows')

Signed-off-by: Christopher Byrne <salah.coronya@gmail.com>
2026-09-07 20:57:29 -05:00
Christopher Byrne 5018a3a848 meson.build: Reflect usage of 0.59 Meson features
src/fe-gtk/meson.build calls feature_opt.allowed() which wasn't
added until 0.59.

Signed-off-by: Christopher Byrne <salah.coronya@gmail.com>
2026-09-07 20:57:29 -05:00
Christopher Byrne df1c74a91d data/misc/meson.build: Fix referencing possibly undefined variable "appstreamcli"
If meson is passed -Dinstall-plugin-metainfo=false, the "appstreamcli"
will not be defined, but it is later referenced in global scope, which
causes and "undefined variable" error in Meson

Signed-off-by: Christopher Byrne <salah.coronya@gmail.com>
2026-09-07 20:57:21 -05:00
73 changed files with 4471 additions and 147 deletions
+15 -1
View File
@@ -33,7 +33,7 @@ jobs:
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
breeze-gtk-theme \
@@ -135,6 +135,20 @@ jobs:
cp -a /usr/lib/gtk-3.0/modules AppDir/usr/lib/gtk-3.0/
fi
# HTTPS inline-image downloads use GSocketClient TLS on Linux.
# GLib provides the API, but TLS and system-proxy support live in
# loadable GIO modules shipped by glib-networking. Bundle those
# modules so the AppImage does not depend on the host copy.
gio_module_dir="$(pkg-config --variable=giomoduledir gio-2.0)"
test -n "$gio_module_dir"
test -f "${gio_module_dir}/libgiognutls.so"
install -d "AppDir${gio_module_dir}"
cp -a "${gio_module_dir}"/libgio*.so "AppDir${gio_module_dir}/"
gio-querymodules "AppDir${gio_module_dir}"
test -f "AppDir${gio_module_dir}/libgiognutls.so"
test -s "AppDir${gio_module_dir}/giomodule.cache"
- name: Verify bundled plugins
run: |
set -eux
+1 -1
View File
@@ -1 +1 @@
2.19.0
2.20.0-dev
+1 -1
View File
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
metainfodir = join_paths(get_option('datadir'), 'metainfo')
mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
desktop_utils = find_program('desktop-file-validate', required: false)
appstreamcli = find_program('appstreamcli', required: false)
if get_option('gtk-frontend')
if get_option('install-appdata')
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
install_dir: metainfodir
)
appstreamcli = find_program('appstreamcli', required: false)
if appstreamcli.found()
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
args: ['validate', zoitechat_appdata]
@@ -29,6 +29,22 @@
<id>zoitechat.desktop</id>
</provides>
<releases>
<release date="2026-09-15" version="2.19.1">
<description>
<ul>
<li>Fixed /msg messages showing twice on servers using IRCv3 <code>echo-message</code>.</li>
<li>Fixed escaping a leading command slash so <code>//text</code> sends <code>/text</code> as normal chat.</li>
<li>Fixed automatic away clearing when sending messages on servers using <code>echo-message</code>.</li>
<li>Fixed other users' away status not updating until rejoining a channel when <code>away-notify</code> updates are missed.</li>
<li>Fixed private messages and <code>/me</code> actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.</li>
<li>Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.</li>
<li>Fixed optional Windows 10 theme downloads in the Windows installer.</li>
<li>Fixed the GTK warning when switching from channel tabs to the channel tree.</li>
<li>Fixed GTK3 theme archive root detection so themes with an <code>index.theme</code> file are preferred correctly.</li>
<li>Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.</li>
</ul>
</description>
</release>
<release date="2026-07-28" version="2.19.0">
<description>
<ul>
+153
View File
@@ -78,6 +78,11 @@ a {
text-decoration: none;
}
::selection {
background: var(--accent);
color: #fff8ea;
}
button,
input,
textarea,
@@ -139,6 +144,7 @@ select {
z-index: 2000;
border-bottom: 3px solid var(--rule);
background: rgba(238, 229, 211, 0.94);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
@@ -231,6 +237,17 @@ select {
font-weight: 900;
}
.site-nav a[aria-current="page"] {
border-block-color: var(--rule);
background: var(--panel-dark);
color: var(--paper-warm);
box-shadow: inset 0 -0.3rem 0 var(--accent);
}
.site-nav a[aria-current="page"]::before {
color: #d19b25;
}
.site-nav a:hover,
.site-nav a:focus-visible {
border-block-color: var(--rule);
@@ -511,6 +528,7 @@ select {
font-weight: 900;
line-height: 1.18;
box-shadow: 4px 4px 0 var(--rule);
touch-action: manipulation;
transition: transform 0.12s ease, background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
@@ -527,6 +545,11 @@ select {
outline: none;
}
.button:active {
transform: translate(3px, 3px);
box-shadow: none;
}
.button-primary {
background: var(--accent);
color: #fff8ea;
@@ -1176,6 +1199,11 @@ select {
color: rgba(255, 248, 234, 0.82);
}
.footer-links a {
width: fit-content;
border-bottom: 1px solid transparent;
}
.footer-links a::before {
color: #d19b25;
content: "↳ ";
@@ -1184,6 +1212,7 @@ select {
.footer-links a:hover,
.footer-links a:focus-visible {
color: #fff8ea;
border-bottom-color: currentColor;
outline: none;
}
@@ -1346,6 +1375,70 @@ select {
animation-iteration-count: 1 !important;
}
}
/* Automatic dark appearance, matching the main ZoiteChat site palette. */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--paper: #161514;
--paper-warm: #f4ead9;
--paper-cool: #1c2522;
--ink: #f2eadb;
--ink-soft: #ddd2c1;
--muted: #bdb19f;
--quiet: #928777;
--rule: #080706;
--rule-soft: rgba(242, 234, 219, 0.16);
--rule-mid: rgba(242, 234, 219, 0.3);
--panel: #211e1a;
--panel-alt: #2b261f;
--panel-dark: #0b0c0c;
--accent: #c34720;
--accent-dark: #ff966d;
--accent-soft: rgba(195, 71, 32, 0.17);
--green: #367662;
--blue: #386b8d;
--warning: #9b6508;
--success: #43856a;
--shadow: 8px 8px 0 rgba(0, 0, 0, 0.82);
--shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.36);
}
html {
background: var(--paper);
}
body {
background:
linear-gradient(90deg, rgba(242, 234, 219, 0.025) 1px, transparent 1px) 0 0 / 68px 68px,
linear-gradient(180deg, rgba(242, 234, 219, 0.022) 1px, transparent 1px) 0 0 / 68px 68px,
radial-gradient(circle at 11% 18%, rgba(195, 71, 32, 0.15), transparent 24rem),
radial-gradient(circle at 86% 4%, rgba(54, 118, 98, 0.16), transparent 28rem),
linear-gradient(180deg, #181715 0%, #111311 58%, #191714 100%);
}
body::before {
background:
repeating-linear-gradient(90deg, transparent 0 17px, rgba(242, 234, 219, 0.025) 17px 18px),
repeating-linear-gradient(0deg, transparent 0 5px, rgba(255, 255, 255, 0.025) 5px 6px);
mix-blend-mode: screen;
}
.site-header {
background: rgba(22, 21, 20, 0.94);
}
.site-nav a,
.nav-menu summary {
background: rgba(39, 35, 30, 0.52);
}
.eyebrow::before,
.footer-links a::before {
color: #e0aa3c;
}
}
/* Sphinx/Read the Docs bridge: aggressively map documentation onto the main ZoiteChat site skin. */
html {
font-size: 16px !important;
@@ -1485,6 +1578,17 @@ body a {
font-weight: 900 !important;
}
#navbar-top .site-nav a[aria-current="page"] {
border-block-color: var(--rule) !important;
background: var(--panel-dark) !important;
color: var(--paper-warm) !important;
box-shadow: inset 0 -0.3rem 0 var(--accent) !important;
}
#navbar-top .site-nav a[aria-current="page"]::before {
color: #d19b25 !important;
}
#navbar-top .site-nav a:hover,
#navbar-top .site-nav a:focus-visible,
#navbar-top .site-nav summary:hover,
@@ -1671,6 +1775,7 @@ footer.site-footer-lite {
}
.document .body p {
max-width: 78ch;
margin: 0 0 1rem !important;
}
@@ -1716,6 +1821,7 @@ ul.search a:focus-visible {
.document .body ul,
.document .body ol {
max-width: 78ch;
margin: 0.75rem 0 1.4rem !important;
padding-left: 1.35rem !important;
}
@@ -1865,6 +1971,11 @@ ul.search a:focus-visible {
text-transform: uppercase !important;
}
.document .body table tbody tr:hover td,
.document .body table tbody tr:focus-within td {
background: var(--accent-soft) !important;
}
.admonition,
.note,
.warning,
@@ -2004,6 +2115,7 @@ input[type="submit"],
line-height: 1.18 !important;
text-shadow: none !important;
box-shadow: 4px 4px 0 var(--rule) !important;
touch-action: manipulation;
}
.btn:hover,
@@ -2019,6 +2131,13 @@ input[type="submit"]:focus-visible {
outline: none !important;
}
.btn:active,
button:active,
input[type="submit"]:active {
transform: translate(3px, 3px) !important;
box-shadow: none !important;
}
#navbar-related {
min-height: auto !important;
margin: 1rem 0 1.5rem !important;
@@ -2275,3 +2394,37 @@ footer:not(.site-footer) {
letter-spacing: -0.04em !important;
}
}
/* Keep basicstrap's stronger overrides readable with the main site's dark palette. */
@media (prefers-color-scheme: dark) {
#navbar-top.site-header {
background: rgba(22, 21, 20, 0.94) !important;
}
#navbar-top .site-nav a,
#navbar-top .site-nav summary {
background: rgba(39, 35, 30, 0.52) !important;
}
.document::after,
.sidebar::after,
#navbar-related::after {
background: linear-gradient(135deg, transparent 50%, rgba(242, 234, 219, 0.09) 50%) !important;
}
input[type="text"],
input[type="search"],
.search-query,
.form-control,
textarea,
select {
background: var(--panel) !important;
box-shadow: inset 3px 3px 0 rgba(0, 0, 0, 0.32) !important;
}
.warning,
.caution,
.danger {
background: #3a2e19 !important;
}
}
+5 -1
View File
@@ -209,6 +209,8 @@
{%- endif %}
<meta name="description" content="">
<meta name="author" content="">
<meta name="theme-color" content="#eee5d3" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#161514" media="(prefers-color-scheme: dark)">
{{ css() }}
{%- if not embedded %}
@@ -260,8 +262,9 @@
<nav class="site-nav" aria-label="Primary">
<a href="https://zoitechat.org/">Home</a>
<a href="https://zoitechat.org/addons.php">Addons</a>
<a href="https://zoitechat.org/announcements.php">Announcements</a>
<a href="https://zoitechat.org/download.php">Download</a>
<a href="{{ pathto(master_doc) }}">Docs</a>
<a href="{{ pathto(master_doc) }}" aria-current="page">Docs</a>
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">GitHub</a>
<details class="nav-menu">
<summary>IRC Support</summary>
@@ -336,6 +339,7 @@
<div class="footer-links">
<a href="https://zoitechat.org/download.php">Download</a>
<a href="https://zoitechat.org/addons.php">Addons</a>
<a href="https://zoitechat.org/announcements.php">Announcements</a>
<a href="https://zoitechat.org/privacy.php">Privacy Policy</a>
<a href="{{ pathto(master_doc) }}">Documentation</a>
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">Source code</a>
+14
View File
@@ -1,6 +1,20 @@
ZoiteChat ChangeLog
=================
2.19.1 (2026-09-15)
-------------------
- Fixed ``/msg`` messages showing twice on servers using IRCv3 ``echo-message``.
- Fixed escaping a leading command slash so ``//text`` sends ``/text`` as normal chat.
- Fixed automatic away clearing when sending messages on servers using ``echo-message``.
- Fixed other users' away status not updating until rejoining a channel when ``away-notify`` updates are missed.
- Fixed private messages and ``/me`` actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.
- Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.
- Fixed optional Windows 10 theme downloads in the Windows installer.
- Fixed the GTK warning when switching from channel tabs to the channel tree.
- Fixed GTK3 theme archive root detection so themes with an ``index.theme`` file are preferred correctly.
- Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.
2.19.0 (2026-07-28)
-------------------
+30
View File
@@ -0,0 +1,30 @@
# Optional Python asyncio integration
`run_async(coroutine, context=None, interval=10)` returns an AsyncTask with
`done()`, `cancel()` and `result()`. Each invocation owns a separate event loop
pumped by an existing main-loop timer. A ready stop callback ensures selector
polling never blocks the UI. The captured context is restored after every pump;
a closed context cancels the work. No process-wide event-loop policy is changed.
Existing synchronous scripts are unaffected. Use `asyncio.get_running_loop()`
inside the coroutine. Child tasks use the same loop/context and are cancelled
when the main coroutine finishes or the script unloads. Cancellation cleanup is
bounded; tasks must not suppress cancellation indefinitely. Cleanup uses the
captured context while it remains open; after it closes, cancellation cleanup uses
the current valid context. Use explicit Context methods for context-specific
output in finally blocks, and do not issue context-dependent commands there. This provides normal
asyncio sockets/timers, not Windows subprocess support or a new HTTP library.
Asyncio hostname resolution can use its executor; use a resolver appropriate to
your library if you require no worker threads. Blocking work inside a coroutine
still blocks the client and must be avoided.
```python
import asyncio
import zoitechat
async def later():
await asyncio.sleep(1)
zoitechat.prnt('Still in the context where this task was started')
task = zoitechat.run_async(later())
```
+35
View File
@@ -0,0 +1,35 @@
# Script-supported capabilities
`register_capability(name, userdata=None, connection_id=None)` returns a normal
hook handle. Register before connecting/reconnecting. Core requests the name only
if advertised in CAP LS or CAP NEW. Existing built-in requests remain enabled;
SASL and STS registration is rejected so scripts cannot change authentication or
transport policy through this API. None/-1 means any connection (zero is a valid
connection ID). Names are lowercase ASCII alphanumerics plus - . / _, 1-200 bytes.
Duplicate registrations do not duplicate requests for an advertised token.
`unhook(handle)` or script/plugin unload removes future request ownership. It
intentionally does not disable a capability already acknowledged on a live
connection, because another plugin/core may depend on it. Registering after CAP
LS does not retrospectively negotiate an already-advertised capability: reconnect
or wait for CAP NEW. Core retains ownership of CAP END; this API does not extend
SASL waiting or delay registration awaiting plugin-only CAP ACKs.
`hook_capability(name, callback, userdata=None, priority=PRI_NORM)` independently
observes negotiation through existing raw CAP hooks. Callback receives
`(CapabilityEvent, userdata)` with name, subcommand, value, enabled, context and
connection_id. LS/NEW mean advertised (`enabled=None`), ACK/LIST mean enabled
(unless a negative ACK), NAK means rejected and DEL means removed. Callback
returns are ignored so this observer cannot eat core negotiation. Existing raw
hooks may still eat CAP before this hook according to their normal priorities.
C: `zoitechat_register_capability(ph, name, flags, connection_id, userdata)`;
flags=0, ID=-1 for any. Check get_info("api_plugin_caps") for "1" before using
the appended Windows slot. Use existing server CAP hooks for notifications.
Requests are split at complete token boundaries within the existing wire limit.
```python
registration = zoitechat.register_capability('draft/example')
zoitechat.hook_capability('draft/example', lambda event, data:
event.context.prnt('{}: {}'.format(event.subcommand, event.enabled)))
```
@@ -0,0 +1,24 @@
# Optional plugin API additions (discussion #144)
Existing hooks, callbacks, return values, context lookup, strings and menu behaviour
remain unchanged. The C plugin function table gains four fixed append-only slots;
unused slots are NULL. Older binaries use the unchanged prefix. A plugin must check
the documented get_info() feature key before accessing a new slot on an older host.
New Python modules are installed by Meson and the existing Windows copy/installer
pipeline. Only modules present in the installation are exported; imports of
zoitechat, hexchat and xchat retain their compatibility aliases.
All APIs run on the client main thread unless explicitly documented otherwise.
New reserved flags must be zero. These changes do not make old API calls thread-safe.
Nested hook dispatch defers deleted-hook reclamation until its outermost return,
so new observers can safely emit events and unhook themselves or other hooks.
The optional asyncio module requires Python 3.7 or newer; older supported
interpreters skip that module and retain the existing scripting API.
Python reload drops the script/API modules while retaining the embedding module
so callback and lib.ph pointer types keep the same CFFI identity.
Server-hook insertion matches both raw and attrs hooks so documented priorities
are honoured for mixed registrations, including new filtered attrs callbacks.
+24
View File
@@ -0,0 +1,24 @@
# Connection and context navigation
`get_connections()` returns one Connection snapshot per live connection:
`id`, `network`, `server`, `context`. Connection IDs distinguish simultaneous
connections with identical names. Prefer its server-tab context where present.
`get_connection(context=None)` finds the owner of a live context.
`get_contexts(connection_id=None, types=None)` lists live contexts, optionally
filtered by connection ID (or a Connection object) and session types: server=1,
channel=2, query=3. `connection.contexts(types=None)` is a convenience method.
`find_context_by_id(connection_id, channel=None)` / `connection.find(channel)`
resolve using that connection's IRC comparator; no channel selects the server tab.
Missing/closed contexts return None. Existing find_context() remains unchanged.
These helpers use the existing channels list's ID/context fields, so no new C
context ABI is needed. Snapshots are not persistent IDs across client restarts;
refresh them after closing/recreating connections. Always check Context.set()
when explicitly selecting a context that might have closed.
```python
connection = zoitechat.get_connection()
for context in connection.contexts(types=(2,)):
context.prnt('A channel on this exact connection')
```
+26
View File
@@ -0,0 +1,26 @@
# Semantic IRC hooks
`on_join`, `on_part`, `on_quit`, `on_nick`, `on_kick`, `on_message`, and
`on_notice` accept `(callback, userdata=None, priority=PRI_NORM)`. The general
form is `hook_event(command, callback, userdata=None, priority=PRI_NORM)`.
Callbacks receive `(event, userdata)`, where the immutable IRCEvent has
command, prefix, nick, user, host, params, context, connection_id and time.
`params` preserves the trailing parameter, including spaces; JOIN parameters
also retain extended-join account/realname data when supplied by the server.
These wrappers use existing server hooks, before core command processing;
return the ordinary EAT_* constants. EAT_ZOITECHAT suppresses the entire IRC
command (including client state updates), not just its text. Use EAT_NONE for
observation. Other plugins' priority and eating rules remain unchanged.
The context is the core-selected context, which can be the server tab when a
query/channel does not yet exist. QUIT is one event per incoming command,
not one callback per affected channel. `event.time` is the existing server-time
attribute (zero when unavailable); other message tags are not exposed here.
```python
def joined(event, userdata):
event.context.prnt('{} joined {}'.format(event.nick, event.params[0]))
return zoitechat.EAT_NONE
zoitechat.on_join(joined)
```
+19
View File
@@ -0,0 +1,19 @@
# Python descriptor hooks
`hook_fd(fd, callback, userdata=None, flags=FD_READ)` returns an ordinary hook
handle accepted by `unhook()`. Callback arguments are `(fd, ready_flags, userdata)`;
return True to retain the watch and False/None to remove it. Exceptions print a
traceback and remove the watch. Unloading the script removes its watches.
The callback runs in the context captured during registration, and that context
is restored afterwards. A closed context removes the watch.
Use nonblocking IO and bounded reads/writes. The descriptor belongs to the script;
unhook before closing it. On Windows socket descriptors use the default flags;
CRT descriptors need FD_NOTSOCKET. The existing C API uses int descriptors: values
outside its range are rejected rather than truncated. This does not add threads.
Example:
```python
watch = zoitechat.hook_fd(sock.fileno(), on_readable, flags=zoitechat.FD_READ)
# def on_readable(fd, flags, userdata): ...; return True
```
+24
View File
@@ -0,0 +1,24 @@
# Core-filtered hooks
Python: `hook_server_filtered(name, callback, userdata=None, priority=PRI_NORM,
connection_id=None, channel=None)` and `hook_print_filtered(...)`.
Callbacks match the existing attrs variants: `(word, word_eol, userdata, attrs)`.
The connection ID distinguishes duplicate names; None/-1 means any connection
(including connection ID zero). A channel restriction uses that connection's
IRC comparator. None means any context; an empty string is an exact restriction.
C: the corresponding zoitechat_hook_*_filtered functions accept
`(ph, name, pri, flags, connection_id, channel, callback, userdata)`;
flags=0, ID=-1 for any and NULL channel for any. Check
`get_info("api_context_filters")` for "1" before accessing the new Windows slots.
Filtering takes place before Python/interpreter entry. Existing unrestricted
hooks, priority, EAT semantics and unload/unhook handling are unchanged.
Filters refer to the core-selected context, not an IRC target parsed from the
line: unknown queries/channels can still have a server-tab context, and QUIT
has its usual single server-hook context. Connection IDs last only this process.
```python
zoitechat.hook_print_filtered('Channel Message', callback,
connection_id=zoitechat.get_prefs('id'), channel='#zoitechat')
```
+21
View File
@@ -0,0 +1,21 @@
# Explicit IRC comparison keys
`IRCName(text, context=None, casemapping=None, connection_id=None)` is an
immutable helper, not a str subclass. Construct it explicitly for dictionary/set
keys; existing get_info/list/event values remain normal strings.
Equality and hashing include the connection ID and captured casemapping, so
identical-looking names on different connections are different keys. An IRCName
never compares equal to a plain str. `str(key)` / `key.text` recover the spelling.
`irc_casefold(text, casemapping='rfc1459')` supports ascii, strict-rfc1459 and
rfc1459. Only ASCII IRC characters fold; Unicode casefold would be incorrect.
The new `get_info('casemapping')` reports the comparator actually used by core.
Current master handles ASCII and RFC1459; this patch does not change its handling
of advertised strict-rfc1459. An explicit mapping is available for scripts that
need strict-rfc1459 independently. Keys capture their mapping so hashes stay
stable; rebuild a collection if the connection's mapping changes.
```python
seen = {zoitechat.IRCName('[SomeNick]')}
assert zoitechat.IRCName('{somenick}') in seen # RFC1459 connection
```
+21
View File
@@ -0,0 +1,21 @@
# Post-display print observers
Python: `hook_print_after(name, callback, userdata=None, priority=PRI_NORM)`.
The callback is `(word, word_eol, userdata, attrs)`, matching hook_print_attrs().
It runs after display_event() has handled the text, so prnt() follows the original
text. Return values are ignored. Events eaten by a pre-hook or hidden by client
preferences do not invoke these observers. This is post-display notification,
not a notification that the entire incoming IRC command has finished updating state.
C: `zoitechat_hook_print_after(ph, name, priority, flags, callback, userdata)`;
flags must be zero. Check `get_info("api_hook_print_after")` for "1" before
accessing the appended Windows function-table slot. Existing pre-hook timing is
unchanged. Unhook/unload use existing lifecycle functions. Recursive emission
skips an already-running observer to prevent it immediately invoking itself.
```python
def after(word, word_eol, userdata, attrs):
zoitechat.prnt('This follows the original channel message')
zoitechat.hook_print_after('Channel Message', after)
```
+1 -1
View File
@@ -1,6 +1,6 @@
project('zoitechat', 'c',
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
meson_version: '>= 0.55.0',
meson_version: '>= 0.59.0',
default_options: [
'c_std=c17',
'buildtype=debugoptimized',
+2
View File
@@ -710,6 +710,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
fishlim_target_entry = gtk_entry_new();
gtk_widget_set_hexpand(fishlim_target_entry, TRUE);
if (target)
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
@@ -760,6 +761,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_widget_set_hexpand(scroll, TRUE);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_container_add(GTK_CONTAINER(scroll), view);
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
+21
View File
@@ -383,3 +383,24 @@ def find_context(server=None, channel=None):
return None
return Context(ctx)
# Optional additive APIs live in separate modules so builds can select features.
def __load_additive_features():
import importlib
import os
for suffix in ('fd', 'events', 'async', 'names', 'contexts',
'post', 'filters', 'caps'):
if suffix == 'async' and sys.version_info < (3, 7):
continue
name = '_zoitechat_' + suffix
if not os.path.isfile(os.path.join(os.path.dirname(__file__), name + '.py')):
continue
module = importlib.import_module(name)
for symbol in module.__all__:
globals()[symbol] = getattr(module, symbol)
__all__.append(symbol)
__load_additive_features()
+100
View File
@@ -0,0 +1,100 @@
"""Opt-in asyncio tasks pumped without blocking the client main loop."""
import asyncio
import operator
import traceback
import _zoitechat as api
__all__ = ['AsyncTask', 'run_async']
class AsyncTask:
"""A script-owned task and isolated event loop; cancel() is cooperative."""
def __init__(self, coroutine, plugin, context, interval):
self._plugin = plugin
self._context = context
self._loop = asyncio.new_event_loop()
self._task = self._loop.create_task(coroutine)
self._closed = False
self._unload_hook = api.hook_unload(lambda data: self._close())
self._timer_hook = api.hook_timer(interval, self._tick)
def done(self):
return self._task.done()
def cancel(self):
if not self._closed:
return self._task.cancel()
return False
def result(self):
return self._task.result()
def _pump(self):
# A ready stop callback forces the selector's timeout to zero.
self._loop.call_soon(self._loop.stop)
self._loop.run_forever()
def _close(self):
if self._closed:
return
self._closed = True
previous = api.get_context()
self._context.set()
try:
pending = asyncio.all_tasks(self._loop)
for task in pending:
task.cancel()
# Bound cleanup work; tasks must cooperate with cancellation.
for unused in range(3):
if not any(not task.done() for task in pending):
break
self._pump()
for task in pending:
if not task.done():
self._loop.call_exception_handler({
'message': 'Script task ignored cancellation during unload',
'task': task,
})
finally:
self._loop.close()
previous.set()
def _tick(self, userdata):
if self._closed:
return False
previous = api.get_context()
if not self._context.set():
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
try:
self._pump()
if not self._task.done():
return True
try:
self._task.result()
except asyncio.CancelledError:
pass
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
finally:
previous.set()
def run_async(coroutine, context=None, interval=10):
"""Schedule a coroutine in a captured context; return an AsyncTask handle."""
interval = operator.index(interval)
if interval < 1 or interval > 2147483647:
raise ValueError('interval must be a positive C int in milliseconds')
if not asyncio.iscoroutine(coroutine):
raise TypeError('run_async expects a coroutine object')
plugin = api.__get_current_plugin()
return AsyncTask(coroutine, plugin, context or api.get_context(), interval)
+64
View File
@@ -0,0 +1,64 @@
"""Script-owned capability requests and structured negotiation observations."""
from collections import namedtuple
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['CapabilityEvent', 'register_capability', 'hook_capability']
CapabilityEvent = namedtuple('CapabilityEvent',
'name subcommand value enabled context connection_id')
def register_capability(name, userdata=None, connection_id=None):
"""Request an advertised capability on next negotiation; unhook releases ownership."""
connection_id = -1 if connection_id is None else operator.index(connection_id)
if connection_id < -1 or connection_id > 2147483647:
raise ValueError('connection_id must fit the C API; -1 means any')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(None, userdata)
handle = lib.zoitechat_register_capability(lib.ph, name.encode(), 0,
connection_id, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise ValueError('invalid capability name, reserved capability or connection ID')
hook.zoitechat_hook = handle
return id(hook)
def hook_capability(name, callback, userdata=None, priority=api.PRI_NORM):
"""Observe LS/NEW/ACK/NAK/DEL/LIST; this hook does not itself request the capability."""
if not callable(callback):
raise TypeError('callback must be callable')
def dispatch(word, word_eol, data):
if not word_eol:
return api.EAT_NONE
line = word_eol[0]
if line.startswith(':'):
unused, separator, line = line.partition(' ')
parts = line.split(' ', 3)
if len(parts) != 4 or parts[0].upper() != 'CAP':
return api.EAT_NONE
subcommand, tokens = parts[2].upper(), parts[3]
if subcommand not in ('LS', 'NEW', 'ACK', 'NAK', 'DEL', 'LIST'):
return api.EAT_NONE
if tokens.startswith('* '):
tokens = tokens[2:]
tokens = tokens.lstrip(':')
for token in tokens.split():
negative = token.startswith('-')
cap, equals, value = token.lstrip('-').partition('=')
if cap != name:
continue
enabled = None
if subcommand in ('ACK', 'LIST'):
enabled = not negative
elif subcommand in ('DEL', 'NAK'):
enabled = False
event = CapabilityEvent(cap, subcommand, value if equals else None,
enabled, api.get_context(), api.get_prefs('id'))
callback(event, data)
return api.EAT_NONE # observing CAP must not suppress core negotiation
return api.hook_server('CAP', dispatch, userdata, priority)
+75
View File
@@ -0,0 +1,75 @@
"""Navigate live connections by ID instead of ambiguous server/network names."""
from collections import namedtuple
import operator
import _zoitechat as api
__all__ = ['Connection', 'get_connections', 'get_connection', 'get_contexts',
'find_context_by_id']
class Connection(namedtuple('_Connection', 'id network server context')):
__slots__ = ()
def contexts(self, types=None):
return get_contexts(self.id, types)
def find(self, channel=None):
return find_context_by_id(self.id, channel)
def _id(connection):
return operator.index(connection.id if isinstance(connection, Connection) else connection)
def get_connections():
"""Return one snapshot per live connection, including disconnected ones."""
entries = api.get_list('channels')
by_id = {}
order = []
for entry in entries:
if entry.id not in by_id:
order.append(entry.id)
if entry.id not in by_id or entry.type == 1:
by_id[entry.id] = Connection(entry.id, entry.network, entry.server,
entry.context)
return [by_id[connection_id] for connection_id in order]
def get_connection(context=None):
"""Find the owning connection of a Context; return None when it has closed."""
context = context or api.get_context()
for entry in api.get_list('channels'):
if entry.context == context:
for connection in get_connections():
if connection.id == entry.id:
return connection
return None
def get_contexts(connection_id=None, types=None):
"""Return contexts, optionally restricted to a connection and session types."""
if connection_id is not None:
connection_id = _id(connection_id)
if types is not None:
types = frozenset(types)
return [entry.context for entry in api.get_list('channels')
if (connection_id is None or entry.id == connection_id)
and (types is None or entry.type in types)]
def find_context_by_id(connection_id, channel=None):
"""Resolve a live context on exactly one connection; None selects its server tab."""
connection_id = _id(connection_id)
previous = api.get_context()
try:
for entry in api.get_list('channels'):
if entry.id != connection_id:
continue
if channel is None:
if entry.type == 1:
return entry.context
elif entry.context.set() and api.nickcmp(entry.channel, channel) == 0:
return entry.context
finally:
previous.set()
return None
+77
View File
@@ -0,0 +1,77 @@
"""Structured, opt-in hooks before the IRC command's core processing."""
from collections import namedtuple
import _zoitechat as api
__all__ = ['IRCEvent', 'hook_event', 'on_join', 'on_part', 'on_quit',
'on_nick', 'on_kick', 'on_message', 'on_notice']
IRCEvent = namedtuple('IRCEvent', 'command prefix nick user host params context connection_id time')
_SUPPORTED = frozenset(('JOIN', 'PART', 'QUIT', 'NICK', 'KICK', 'PRIVMSG', 'NOTICE'))
def _parse(line):
prefix = ''
if line.startswith(':'):
prefix, separator, line = line[1:].partition(' ')
if not separator:
return None
command, separator, rest = line.partition(' ')
params = []
while rest:
rest = rest.lstrip(' ')
if rest.startswith(':'):
params.append(rest[1:])
break
param, separator, rest = rest.partition(' ')
if param:
params.append(param)
nick, marker, userhost = prefix.partition('!')
user, marker, host = userhost.partition('@')
return command.upper(), prefix, nick, user, host, tuple(params)
def hook_event(command, callback, userdata=None, priority=api.PRI_NORM):
"""callback(event, userdata) returns existing EAT_* constants."""
command = command.upper()
if command not in _SUPPORTED:
raise ValueError('unsupported semantic event: ' + command)
if not callable(callback):
raise TypeError('callback must be callable')
def dispatch(word, word_eol, data, attrs):
if not word_eol:
return api.EAT_NONE
parsed = _parse(word_eol[0])
if parsed is None or parsed[0] != command:
return api.EAT_NONE
event = IRCEvent(*(parsed + (api.get_context(), api.get_prefs('id'), attrs.time)))
return callback(event, data)
return api.hook_server_attrs(command, dispatch, userdata, priority)
def on_join(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('JOIN', callback, userdata, priority)
def on_part(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('PART', callback, userdata, priority)
def on_quit(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('QUIT', callback, userdata, priority)
def on_nick(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('NICK', callback, userdata, priority)
def on_kick(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('KICK', callback, userdata, priority)
def on_message(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('PRIVMSG', callback, userdata, priority)
def on_notice(callback, userdata=None, priority=api.PRI_NORM):
return hook_event('NOTICE', callback, userdata, priority)
+39
View File
@@ -0,0 +1,39 @@
"""File descriptor readiness hooks; callbacks execute on the main thread."""
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['FD_READ', 'FD_WRITE', 'FD_EXCEPTION', 'FD_NOTSOCKET', 'hook_fd']
FD_READ, FD_WRITE, FD_EXCEPTION, FD_NOTSOCKET = 1, 2, 4, 8
def hook_fd(fd, callback, userdata=None, flags=FD_READ):
"""callback(fd, ready_flags, userdata): True keeps the watch, False removes it."""
fd = operator.index(fd)
flags = operator.index(flags)
if fd < 0 or fd > 2147483647:
raise ValueError('fd must fit the C API nonnegative int descriptor')
if flags & ~15 or not flags & 7:
raise ValueError('flags must select READ, WRITE or EXCEPTION')
if not callable(callback):
raise TypeError('callback must be callable')
plugin = api.__get_current_plugin()
context = api.get_context()
def dispatch(descriptor, ready, data):
previous = api.get_context()
if not context.set():
return False
try:
return callback(descriptor, ready, data)
finally:
previous.set()
hook = plugin.add_hook(dispatch, userdata)
handle = lib.zoitechat_hook_fd(lib.ph, fd, flags, lib._on_fd_hook, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to watch descriptor')
hook.zoitechat_hook = handle
return id(hook)
+36
View File
@@ -0,0 +1,36 @@
"""Context restrictions evaluated by core before entering Python."""
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['hook_server_filtered', 'hook_print_filtered']
def _register(name, callback, userdata, priority, connection_id, channel, server):
if not callable(callback):
raise TypeError('callback must be callable')
connection_id = -1 if connection_id is None else operator.index(connection_id)
if connection_id < -1 or connection_id > 2147483647:
raise ValueError('connection_id must fit the C API; -1 means any')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(callback, userdata)
register = lib.zoitechat_hook_server_filtered if server else lib.zoitechat_hook_print_filtered
dispatch = lib._on_server_attrs_hook if server else lib._on_print_attrs_hook
handle = register(lib.ph, name.encode(), priority, 0, connection_id,
ffi.NULL if channel is None else channel.encode(), dispatch, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to install filtered hook')
hook.zoitechat_hook = handle
return id(hook)
def hook_server_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, True)
def hook_print_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
connection_id=None, channel=None):
return _register(name, callback, userdata, priority, connection_id, channel, False)
+74
View File
@@ -0,0 +1,74 @@
"""Explicit immutable IRC names; existing string-returning APIs are unchanged."""
import _zoitechat as api
__all__ = ['IRCName', 'irc_casefold']
def irc_casefold(text, casemapping='rfc1459'):
"""Fold ASCII IRC characters only; never apply Unicode lower()/casefold()."""
if casemapping not in ('ascii', 'strict-rfc1459', 'rfc1459'):
raise ValueError('unsupported IRC casemapping')
extra = {'[': '{', ']': '}', '\\': '|'} if casemapping != 'ascii' else {}
if casemapping == 'rfc1459':
extra['^'] = '~'
return ''.join(chr(ord(c) + 32) if 'A' <= c <= 'Z' else extra.get(c, c)
for c in text)
class IRCName:
"""Equality/hash include connection identity and the captured casemapping."""
__slots__ = ('_text', '_casemapping', '_connection_id', '_key')
def __init__(self, text, context=None, casemapping=None, connection_id=None):
context = context or api.get_context()
if casemapping is None:
casemapping = context.get_info('casemapping')
if casemapping is None:
raise RuntimeError('host does not expose its comparison casemapping')
if connection_id is None:
previous = api.get_context()
if not context.set():
raise ValueError('context has closed')
try:
connection_id = api.get_prefs('id')
finally:
previous.set()
object.__setattr__(self, '_text', text)
object.__setattr__(self, '_casemapping', casemapping)
object.__setattr__(self, '_connection_id', connection_id)
object.__setattr__(self, '_key', (connection_id, casemapping,
irc_casefold(text, casemapping)))
def __setattr__(self, name, value):
raise AttributeError('IRCName is immutable')
@property
def text(self):
return self._text
@property
def casemapping(self):
return self._casemapping
@property
def connection_id(self):
return self._connection_id
def __str__(self):
return self._text
def __repr__(self):
return 'IRCName({!r}, casemapping={!r}, connection_id={!r})'.format(
self._text, self._casemapping, self._connection_id)
def __eq__(self, other):
if not isinstance(other, IRCName):
return NotImplemented
return self._key == other._key
def __ne__(self, other):
result = self.__eq__(other)
return NotImplemented if result is NotImplemented else not result
def __hash__(self):
return hash(self._key)
+21
View File
@@ -0,0 +1,21 @@
"""Opt-in post-display print observers."""
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['hook_print_after']
def hook_print_after(name, callback, userdata=None, priority=api.PRI_NORM):
"""callback(word, word_eol, userdata, attrs); its return value is ignored."""
if not callable(callback):
raise TypeError('callback must be callable')
plugin = api.__get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.zoitechat_hook_print_after(lib.ph, name.encode(), priority, 0,
lib._on_print_attrs_hook, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to install post-display hook')
hook.zoitechat_hook = handle
return id(hook)
+1
View File
@@ -38,6 +38,7 @@ extern "Python" int _on_print_attrs_hook(char **, zoitechat_event_attrs *, void
extern "Python" int _on_server_hook(char **, char **, void *);
extern "Python" int _on_server_attrs_hook(char **, char **, zoitechat_event_attrs *, void *);
extern "Python" int _on_timer_hook(void *);
extern "Python" int _on_fd_hook(int, int, void *);
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
extern "Python" int _on_plugin_deinit(void);
+5 -1
View File
@@ -27,7 +27,11 @@ python3_source = custom_target('python-bindings',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
)
install_data(['_zoitechat.py', 'zoitechat.py', 'xchat.py', 'hexchat.py'],
# Include any selected additive API modules alongside the compatibility modules.
python_api_files = run_command(python3, '-c',
'import glob,sys; print("\\n".join(sorted(glob.glob(sys.argv[1] + "/_zoitechat*.py"))))',
meson.current_source_dir(), check: true).stdout().strip().split('\n')
install_data(python_api_files + ['zoitechat.py', 'xchat.py', 'hexchat.py'],
install_dir: join_paths(get_option('libdir'), 'zoitechat/python')
)
+28 -1
View File
@@ -304,6 +304,28 @@ def _on_timer_hook(userdata):
return 0
@ffi.def_extern()
def _on_fd_hook(fd, flags, userdata):
hook = ffi.from_handle(userdata)
keep = False
try:
keep = bool(hook.callback(fd, flags, hook.userdata))
except Exception:
traceback.print_exc()
if not keep:
# The C watch removes itself on return; do not unhook it a second time.
try:
hook.is_unload = True
# from_handle() yields a weak proxy: id(proxy) is not id(Hook).
for existing in hook.plugin.hooks:
if existing == hook:
hook.plugin.remove_hook(id(existing))
break
except ReferenceError:
pass # the callback already unhooked itself
return int(keep)
@ffi.def_extern()
def _on_say_command(word, word_eol, userdata):
"""Handle input in the special >>python<< tab.
@@ -590,7 +612,12 @@ def _on_plugin_deinit():
sys.stderr = sys.__stderr__
pydoc.help = pydoc.Helper()
for mod in ('_zoitechat', 'zoitechat', 'xchat', '_zoitechat_embedded'):
# CFFI callbacks keep the embedding module's ffi/lib identity for the
# process lifetime; deleting it creates incompatible types on reload.
for mod in ('_zoitechat', 'zoitechat', 'hexchat', 'xchat',
'_zoitechat_fd', '_zoitechat_events', '_zoitechat_async',
'_zoitechat_names', '_zoitechat_contexts', '_zoitechat_post',
'_zoitechat_filters', '_zoitechat_caps'):
try:
del sys.modules[mod]
+1
View File
@@ -52,6 +52,7 @@
<None Include="xchat.py" />
<None Include="hexchat.py" />
<None Include="_zoitechat.py" />
<None Include="_zoitechat_*.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(IntDir)python.c" />
+1
View File
@@ -52,6 +52,7 @@
<None Include="xchat.py" />
<None Include="hexchat.py" />
<None Include="_zoitechat.py" />
<None Include="_zoitechat_*.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(IntDir)python.c" />
+1 -1
View File
@@ -26,7 +26,7 @@ if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system =
sysinfo_cargs += '-DHAVE_LIBPCI'
sysinfo_sources += 'unix/pci.c'
picidsdir = libpci.get_pkgconfig_variable('idsdir')
picidsdir = libpci.get_variable(pkgconfig : 'idsdir')
pciids = join_paths(picidsdir, 'pci.ids')
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
endif
+1
View File
@@ -30,6 +30,7 @@ src/fe-gtk/fe-gtk.c
src/fe-gtk/fkeys.c
src/fe-gtk/gtkutil.c
src/fe-gtk/ignoregui.c
src/fe-gtk/inline-image.c
src/fe-gtk/joind.c
src/fe-gtk/maingui.c
src/fe-gtk/menu.c
+4
View File
@@ -443,6 +443,7 @@ const struct prefs vars[] =
{"gui_tab_chans", P_OFFINT (hex_gui_tab_chans), TYPE_BOOL},
{"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL},
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
{"gui_tab_font", P_OFFSET (hex_gui_tab_font), TYPE_STR},
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
{"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
@@ -565,6 +566,7 @@ const struct prefs vars[] =
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
{"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT},
{"net_remote_media", P_OFFINT (hex_net_remote_media), TYPE_BOOL},
{"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL},
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
@@ -778,6 +780,8 @@ load_default_config(void)
prefs.hex_gui_hide_menu = 1;
#endif
prefs.hex_gui_ctrlq_quit = 1;
/* hex_net_remote_media stays 0: loading remote media can reveal the
user's IP address, so it is strictly opt-in */
prefs.hex_gui_input_attr = 1;
prefs.hex_gui_input_icon = 1;
prefs.hex_gui_input_nick = 1;
+3
View File
@@ -70,6 +70,9 @@ void fe_add_rawlog (struct server *serv, char *text, int len, int outbound);
#define FE_MSG_ERROR 8
#define FE_MSG_MARKUP 16
void fe_message (char *msg, int flags);
#ifdef __linux__
void fe_root_warning (void);
#endif
#define FIA_READ 1
#define FIA_WRITE 2
#define FIA_EX 4
+4 -2
View File
@@ -858,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
static gint
theme_root_candidate_compare (gconstpointer a, gconstpointer b)
{
const ThemeRootCandidate *ca = a;
const ThemeRootCandidate *cb = b;
/* g_ptr_array_sort() passes pointers to the array slots, not the values
* stored in those slots. Dereference once before comparing candidates. */
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
if (ca->has_index_theme != cb->has_index_theme)
return ca->has_index_theme ? -1 : 1;
if (ca->depth != cb->depth)
+74 -4
View File
@@ -41,6 +41,8 @@
#include "network.h"
#include "notify.h"
#include "outbound.h"
#include <gmodule.h>
#include "plugin.h"
#include "inbound.h"
#include "server.h"
#include "servlist.h"
@@ -1962,10 +1964,58 @@ inbound_cap_ack (server *serv, char *nick, char *extensions,
inbound_toggle_caps (serv, extensions, TRUE);
}
/* Split requests only at token boundaries, including plugin-added names. */
static void
inbound_cap_request_append (server *serv, char *buffer, gsize size,
const char *name, time_t timestamp)
{
if (strlen (buffer) + strlen (name) + 1 >= size)
{
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
buffer + 9, NULL, NULL, NULL, 0, timestamp);
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
g_strlcpy (buffer, "CAP REQ :", size);
}
g_strlcat (buffer, name, size);
g_strlcat (buffer, " ", size);
}
static void
inbound_plugin_cap_new (server *serv, const char *extensions, time_t timestamp)
{
char buffer[500] = "CAP REQ :";
char **tokens = g_strsplit (extensions, " ", 0);
GHashTable *seen = g_hash_table_new (g_str_hash, g_str_equal);
int i;
gboolean requested = FALSE;
for (i = 0; tokens[i]; i++)
{
char *value = strchr (tokens[i], '=');
if (value)
*value = '\0';
if (plugin_requests_capability (serv, tokens[i]) &&
!g_hash_table_contains (seen, tokens[i]))
{
g_hash_table_add (seen, tokens[i]);
inbound_cap_request_append (serv, buffer, sizeof (buffer), tokens[i], timestamp);
requested = TRUE;
}
}
if (requested)
{
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
buffer + 9, NULL, NULL, NULL, 0, timestamp);
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
}
g_hash_table_destroy (seen);
g_strfreev (tokens);
}
void
inbound_cap_new (server *serv, char *nick, char *extensions,
const message_tags_data *tags_data)
{
gboolean sts_upgrade_triggered = FALSE;
if (extensions)
{
char **tokens = g_strsplit (extensions, " ", 0);
@@ -1977,7 +2027,7 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
{
sts_handle_capability (serv, parts[1]);
sts_upgrade_triggered |= sts_handle_capability (serv, parts[1]);
}
g_strfreev (parts);
@@ -1986,6 +2036,9 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
g_strfreev (tokens);
}
if (extensions && !sts_upgrade_triggered)
inbound_plugin_cap_new (serv, extensions, tags_data->timestamp);
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
NULL, NULL, 0, tags_data->timestamp);
@@ -2116,6 +2169,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
gboolean want_cap = FALSE; /* format the CAP REQ string based on previous capabilities being requested or not */
gboolean sts_upgrade_triggered = FALSE;
char **extensions;
GString *requests = g_string_new ("");
int i;
if (g_str_has_prefix (extensions_str, "* "))
@@ -2181,7 +2235,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
}
want_cap = TRUE;
serv->waiting_on_sasl = TRUE;
g_strlcat (buffer, "sasl ", sizeof(buffer));
g_string_append (requests, "sasl ");
continue;
}
@@ -2189,28 +2243,44 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
{
if (!g_strcmp0 (extension, supported_caps[x]))
{
g_strlcat (buffer, extension, sizeof(buffer));
g_strlcat (buffer, " ", sizeof(buffer));
g_string_append (requests, extension);
g_string_append_c (requests, ' ');
want_cap = TRUE;
break;
}
}
if (x == G_N_ELEMENTS (supported_caps) && plugin_requests_capability (serv, extension))
{
g_string_append (requests, extension);
g_string_append_c (requests, ' ');
want_cap = TRUE;
}
}
g_strfreev (extensions);
if (sts_upgrade_triggered)
{
g_string_free (requests, TRUE);
return;
}
if (want_cap)
{
char **tokens = g_strsplit (requests->str, " ", 0);
/* Defer every batch until STS has decided whether to reconnect. */
for (i = 0; tokens[i]; i++)
if (tokens[i][0])
inbound_cap_request_append (serv, buffer, sizeof (buffer), tokens[i], tags_data->timestamp);
g_strfreev (tokens);
/* buffer + 9 = emit buffer without "CAP REQ :" */
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
buffer + 9, NULL, NULL, NULL, 0,
tags_data->timestamp);
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
}
g_string_free (requests, TRUE);
if (!serv->waiting_on_sasl && !serv->waiting_on_cap)
{
/* if we use SASL, CAP END is dealt via raw numerics */
+8 -6
View File
@@ -2826,6 +2826,7 @@ static gboolean
client_tag_allowed (server *serv, const char *tag)
{
char **deny;
gboolean blocked = FALSE;
int i;
if (!serv->have_message_tags)
@@ -2837,15 +2838,16 @@ client_tag_allowed (server *serv, const char *tag)
deny = g_strsplit (serv->clienttagdeny, ",", 0);
for (i = 0; deny[i]; i++)
{
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
{
g_strfreev (deny);
return FALSE;
}
if (!strcmp (deny[i], "*"))
blocked = TRUE;
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
blocked = FALSE;
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
blocked = TRUE;
}
g_strfreev (deny);
return TRUE;
return !blocked;
}
static char *
+149 -2
View File
@@ -99,6 +99,10 @@ struct _zoitechat_hook
int tag; /* for timers & FDs only */
int type; /* HOOK_* */
int pri; /* fd */ /* priority / fd for HOOK_FD only */
int cap_connection_id;
int connection_filter;
char *channel_filter;
gboolean context_filtered;
};
struct _zoitechat_list
@@ -143,6 +147,8 @@ enum
HOOK_PRINT_ATTRS = 1 << 4, /* same as above, with attributes */
HOOK_TIMER = 1 << 5, /* timeouts */
HOOK_FD = 1 << 6, /* sockets & fds */
HOOK_PRINT_AFTER = 1 << 8, /* opt-in post-display observers */
HOOK_CAPABILITY = 1 << 9, /* script-owned CAP requests */
HOOK_DELETED = 1 << 7 /* marked for deletion */
};
@@ -176,6 +182,7 @@ enum
GSList *plugin_list = NULL; /* export for plugingui.c */
static GSList *hook_list = NULL;
static unsigned int plugin_dispatch_depth;
extern const struct prefs vars[]; /* cfgfiles.c */
@@ -243,7 +250,7 @@ plugin_list_add (zoitechat_context *ctx, char *filename, const char *name,
{
zoitechat_plugin *pl;
pl = g_new (zoitechat_plugin, 1);
pl = g_new0 (zoitechat_plugin, 1);
pl->handle = handle;
pl->filename = filename;
pl->context = ctx;
@@ -350,6 +357,10 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func,
pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs;
pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create;
pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free;
pl->zoitechat_register_capability = zoitechat_register_capability;
pl->zoitechat_hook_server_filtered = zoitechat_hook_server_filtered;
pl->zoitechat_hook_print_filtered = zoitechat_hook_print_filtered;
pl->zoitechat_hook_print_after = zoitechat_hook_print_after;
/* run zoitechat_plugin_init, if it returns 0, close the plugin */
if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0)
@@ -610,6 +621,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
zoitechat_hook *hook;
int ret, eat = 0;
plugin_dispatch_depth++;
list = hook_list;
while (1)
{
@@ -619,6 +631,22 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
hook = list->data;
next = list->next;
if (hook->context_filtered &&
(!is_session (sess) ||
(hook->connection_filter != -1 && hook->connection_filter != sess->server->id) ||
(hook->channel_filter && sess->server->p_cmp (hook->channel_filter, sess->channel))))
{
list = next;
continue; /* filter in C before entering an interpreter */
}
if (type == HOOK_PRINT_AFTER && hook->tag)
{
list = next;
continue; /* no recursive invocation of this same observer */
}
if (type == HOOK_PRINT_AFTER)
hook->tag = 1;
hook->pl->context = sess;
/* run the plugin's callback function */
@@ -627,6 +655,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
case HOOK_COMMAND:
ret = ((zoitechat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata);
break;
case HOOK_PRINT_AFTER:
case HOOK_PRINT_ATTRS:
ret = ((zoitechat_print_attrs_cb *)hook->callback) (word, attrs, hook->userdata);
break;
@@ -641,6 +670,15 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
break;
}
if (type == HOOK_PRINT_AFTER)
{
hook->tag = 0;
if (!is_session (sess))
goto xit;
list = next;
continue; /* display already happened; observers cannot eat it */
}
if ((ret & ZOITECHAT_EAT_ZOITECHAT) && (ret & ZOITECHAT_EAT_PLUGIN))
{
eat = 1;
@@ -655,6 +693,10 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
}
xit:
/* Nested callbacks can unhook entries still used by an outer dispatch.
* Keep both hook objects and list links alive until the outermost return. */
if (--plugin_dispatch_depth != 0)
return eat;
/* really remove deleted hooks now */
list = hook_list;
while (list)
@@ -719,6 +761,15 @@ plugin_emit_print (session *sess, char *word[], time_t server_time)
HOOK_PRINT | HOOK_PRINT_ATTRS);
}
void
plugin_emit_print_after (session *sess, char *word[], time_t server_time)
{
zoitechat_event_attrs attrs;
attrs.server_time_utc = server_time;
if (is_session (sess))
plugin_hook_run (sess, word[0], word, NULL, &attrs, HOOK_PRINT_AFTER);
}
int
plugin_emit_dummy_print (session *sess, char *name)
{
@@ -806,7 +857,7 @@ plugin_insert_hook (zoitechat_hook *new_hook)
break;
case HOOK_SERVER:
case HOOK_SERVER_ATTRS:
new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS;
new_hook_type = HOOK_SERVER | HOOK_SERVER_ATTRS;
break;
default:
new_hook_type = new_hook->type;
@@ -1018,10 +1069,87 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook)
g_free (hook->name); /* NULL for timers & fds */
g_free (hook->help_text); /* NULL for non-commands */
g_free (hook->channel_filter);
hook->channel_filter = NULL;
return hook->userdata;
}
zoitechat_hook *
zoitechat_hook_print_after (zoitechat_plugin *ph, const char *name, int pri,
int flags, zoitechat_print_attrs_cb *callback, void *userdata)
{
if (flags != 0 || !name || !callback)
return NULL;
return plugin_add_hook (ph, HOOK_PRINT_AFTER, pri, name, NULL, callback, 0, userdata);
}
static zoitechat_hook *
plugin_add_filtered_hook (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
int type, void *callback, void *userdata)
{
zoitechat_hook *hook;
if (flags != 0 || connection_id < -1 || !name || !callback)
return NULL;
hook = plugin_add_hook (ph, type, pri, name, NULL, callback, 0, userdata);
hook->context_filtered = TRUE;
hook->connection_filter = connection_id;
hook->channel_filter = g_strdup (channel);
return hook;
}
zoitechat_hook *
zoitechat_hook_server_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_serv_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_SERVER_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_hook_print_filtered (zoitechat_plugin *ph, const char *name, int pri,
int flags, int connection_id, const char *channel,
zoitechat_print_attrs_cb *callback, void *userdata)
{
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
HOOK_PRINT_ATTRS, callback, userdata);
}
zoitechat_hook *
zoitechat_register_capability (zoitechat_plugin *ph, const char *name, int flags,
int connection_id, void *userdata)
{
const unsigned char *p;
zoitechat_hook *hook;
if (flags != 0 || connection_id < -1 || !name || !name[0] || strlen (name) > 200 ||
strcmp (name, "sasl") == 0 || strcmp (name, "sts") == 0)
return NULL; /* authentication and transport policy belong to core */
for (p = (const unsigned char *)name; *p; p++)
if (!g_ascii_islower (*p) && !g_ascii_isdigit (*p) &&
*p != '-' && *p != '.' && *p != '/' && *p != '_')
return NULL;
hook = plugin_add_hook (ph, HOOK_CAPABILITY, 0, name, NULL, NULL, 0, userdata);
hook->cap_connection_id = connection_id;
return hook;
}
gboolean
plugin_requests_capability (server *serv, const char *name)
{
GSList *entry;
for (entry = hook_list; entry; entry = entry->next)
{
zoitechat_hook *hook = entry->data;
if (hook && hook->type == HOOK_CAPABILITY &&
(hook->cap_connection_id == -1 || hook->cap_connection_id == serv->id) &&
strcmp (hook->name, name) == 0)
return TRUE;
}
return FALSE;
}
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri,
zoitechat_cmd_cb *callb, const char *help_text, void *userdata)
@@ -1181,6 +1309,25 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e);
}
/* Report the comparison actually used by this connection, not an
* advertised mapping that the current core does not implement. */
if (strcmp (id, "casemapping") == 0)
{
if (!is_session (ph->context))
return NULL;
return ph->context->server->p_cmp == g_ascii_strcasecmp
? "ascii" : "rfc1459";
}
if (strcmp (id, "api_hook_print_after") == 0)
return "1";
if (strcmp (id, "api_context_filters") == 0)
return "1";
if (strcmp (id, "api_plugin_caps") == 0)
return "1";
hash = str_hash (id);
/* do the session independant ones first */
switch (hash)
+11
View File
@@ -150,6 +150,15 @@ struct _zoitechat_plugin
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
zoitechat_event_attrs *attrs);
/* Additive API slots: append only; old binary offsets are unchanged. */
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
/* PRIVATE FIELDS! */
void *handle; /* from dlopen */
char *filename; /* loaded from */
@@ -174,6 +183,8 @@ int plugin_emit_command (session *sess, char *name, char *word[], char *word_eol
int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[],
time_t server_time);
int plugin_emit_print (session *sess, char *word[], time_t server_time);
gboolean plugin_requests_capability (server *serv, const char *name);
void plugin_emit_print_after (session *sess, char *word[], time_t server_time);
int plugin_emit_dummy_print (session *sess, char *name);
int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, gunichar key);
GList* plugin_command_list(GList *tmp_list);
+1
View File
@@ -2306,6 +2306,7 @@ text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
if (!prefs.hex_away_omit_alerts || !sess->server->is_away)
sound_play_event (index);
display_event (sess, index, word, stripcolor_args, timestamp);
plugin_emit_print_after (sess, word, timestamp);
}
char *
+33
View File
@@ -193,10 +193,39 @@ struct _zoitechat_plugin
zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph);
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
zoitechat_event_attrs *attrs);
/* Additive API slots: append only; old binary offsets are unchanged. */
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
};
#endif
/* api_hook_print_after=1: observer returns are ignored; flags must be zero. */
zoitechat_hook *zoitechat_hook_print_after (zoitechat_plugin *ph,
const char *name, int pri, int flags,
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
/* api_context_filters=1: connection_id -1 is any; NULL channel is any. */
zoitechat_hook *zoitechat_hook_server_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], char *word_eol[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
zoitechat_hook *zoitechat_hook_print_filtered (zoitechat_plugin *ph,
const char *name, int pri, int flags, int connection_id, const char *channel,
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
void *userdata);
/* api_plugin_caps=1: register before CAP LS; flags=0, connection_id=-1 for any. */
zoitechat_hook *zoitechat_register_capability (zoitechat_plugin *ph,
const char *name, int flags, int connection_id, void *userdata);
zoitechat_hook *
zoitechat_hook_command (zoitechat_plugin *ph,
const char *name,
@@ -416,6 +445,10 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph,
#define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server)
#define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs)
#define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print)
#define zoitechat_register_capability ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_register_capability)
#define zoitechat_hook_server_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_filtered)
#define zoitechat_hook_print_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_filtered)
#define zoitechat_hook_print_after ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_after)
#define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs)
#define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer)
#define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)
+22 -3
View File
@@ -95,6 +95,7 @@ int zoitechat_is_quitting = FALSE;
/* command-line args */
int arg_dont_autoconnect = FALSE;
int arg_skip_plugins = FALSE;
gint arg_run_as_root = FALSE;
char *arg_url = NULL;
char **arg_urls = NULL;
char *arg_command = NULL;
@@ -463,8 +464,7 @@ doover:
while (list)
{
sess = list->data;
if (!sess->server->have_awaynotify)
sess->done_away_check = FALSE;
sess->done_away_check = FALSE;
list = list->next;
}
loop++;
@@ -1176,6 +1176,25 @@ main (int argc, char *argv[])
HRESULT coinit_result;
#endif
#ifdef __linux__
for (i = 1; i < argc; i++)
{
if (strcmp (argv[i], "--") == 0)
break;
if (strcmp (argv[i], "--run-as-root") == 0)
{
arg_run_as_root = TRUE;
break;
}
}
if (geteuid () == 0 && !arg_run_as_root)
{
fe_root_warning ();
return EXIT_FAILURE;
}
#endif
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
@@ -1264,7 +1283,7 @@ main (int argc, char *argv[])
#ifndef WIN32
#ifndef __EMX__
/* OS/2 uses UID 0 all the time */
if (getuid () == 0)
if (getuid () == 0 && !arg_run_as_root)
fe_message (_("* Running IRC as root is stupid! You should\n"
" create a User Account and use that to login.\n"), FE_MSG_WARN|FE_MSG_WAIT);
#endif
+3
View File
@@ -204,6 +204,7 @@ struct zoitechatprefs
unsigned int hex_net_auto_reconnect;
unsigned int hex_net_auto_reconnectonfail;
unsigned int hex_net_proxy_auth;
unsigned int hex_net_remote_media;
unsigned int hex_net_throttle;
unsigned int hex_notify_whois_online;
unsigned int hex_perl_warnings;
@@ -314,6 +315,7 @@ struct zoitechatprefs
char hex_dcc_completed_dir[PATHLEN + 1];
char hex_dcc_dir[PATHLEN + 1];
char hex_dcc_ip[DOMAINLEN + 1];
char hex_gui_tab_font[FONTNAMELEN + 1];
char hex_gui_ulist_doubleclick[256];
char hex_input_command_char[4];
char hex_irc_extra_hilight[300];
@@ -448,6 +450,7 @@ typedef struct session
int mode_timeout_tag;
int typing_timeout_tag;
int typing_status;
gint64 typing_last_sent;
int typing_animation_tag;
int typing_animation_frame;
+1
View File
@@ -25,6 +25,7 @@ extern struct zoitechatprefs prefs;
extern int zoitechat_is_quitting;
extern gint arg_skip_plugins; /* command-line args */
extern gint arg_dont_autoconnect;
extern gint arg_run_as_root;
extern char *arg_url;
extern char **arg_urls;
extern char *arg_command;
+3 -3
View File
@@ -138,7 +138,7 @@ ascii_open (void)
{
table_pos++;
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_container_add (GTK_CONTAINER (vbox), hbox);
gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
gtk_widget_show (hbox);
i++;
continue;
@@ -155,14 +155,14 @@ ascii_open (void)
G_CALLBACK (ascii_click), NULL);
g_signal_connect (G_OBJECT (but), "enter-notify-event",
G_CALLBACK (ascii_enter), label);
gtk_box_pack_start (GTK_BOX (hbox), but, 0, 0, 0);
gtk_box_pack_start (GTK_BOX (hbox), but, TRUE, TRUE, 0);
gtk_widget_show (but);
table_pos += len;
}
frame = gtk_frame_new ("");
gtk_container_add (GTK_CONTAINER (hbox), frame);
gtk_box_pack_start (GTK_BOX (hbox), frame, TRUE, TRUE, 0);
gtk_container_add (GTK_CONTAINER (frame), label);
gtk_widget_show (label);
gtk_widget_show (frame);
+21
View File
@@ -19,6 +19,8 @@
/* file included in chanview.c */
#include "theme/theme-css.h"
typedef struct
{
GtkWidget *outer; /* outer box */
@@ -690,6 +692,7 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
GtkWidget *label;
GtkWidget *close_button;
GtkWidget *close_icon;
GtkCssProvider *close_css;
but = gtk_toggle_button_new ();
gtk_widget_set_name (but, "zoitechat-tab");
@@ -698,9 +701,27 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
cv_add_scroll_events (but);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
label = gtk_label_new (name);
if (prefs.hex_gui_tab_font[0])
{
PangoFontDescription *font_desc;
/* Keep activity colours and the Smaller text scale in the label attributes. */
font_desc = pango_font_description_from_string (prefs.hex_gui_tab_font);
gtkutil_apply_palette (label, NULL, NULL, font_desc);
pango_font_description_free (font_desc);
}
close_button = gtk_button_new ();
cv_add_scroll_events (close_button);
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "zoitechat-tab-close");
/* Theme button minimums and padding must not determine the tab height. */
close_css = gtk_css_provider_new ();
gtk_css_provider_load_from_data (close_css,
".zoitechat-tab-close { min-width: 0; min-height: 0; padding: 1px; margin: 0; }",
-1, NULL);
theme_css_apply_widget_provider (close_button, GTK_STYLE_PROVIDER (close_css));
g_object_unref (close_css);
gtk_widget_set_valign (close_button, GTK_ALIGN_CENTER);
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
gtk_button_set_always_show_image (GTK_BUTTON (close_button), TRUE);
+28 -19
View File
@@ -301,26 +301,35 @@ cv_tree_focus (chan *ch)
path = gtk_tree_model_get_path (model, &ch->iter);
if (path)
{
/* This full section does what
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
* does, except it only scrolls the window if the provided cell is
* not visible. Basic algorithm taken from gtktreeview.c */
/* obtain information to see if the cell is visible */
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
gtk_tree_view_get_visible_rect (tree, &vis_rect);
/* The cordinates aren't offset correctly */
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
/* only need to scroll if out of bounds */
if (cell_rect.y < vis_rect.y ||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
{
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
if (dest_y < 0)
dest_y = 0;
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
/* Layout changes can restore focus before the new tree is realized.
* Let GTK track the row and scroll once its geometry is available. */
gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
}
else
{
/* This full section does what
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
* does, except it only scrolls the window if the provided cell is
* not visible. Basic algorithm taken from gtktreeview.c */
/* obtain information to see if the cell is visible */
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
gtk_tree_view_get_visible_rect (tree, &vis_rect);
/* The cordinates aren't offset correctly */
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
/* only need to scroll if out of bounds */
if (cell_rect.y < vis_rect.y ||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
{
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
if (dest_y < 0)
dest_y = 0;
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
}
}
/* theft done, now make it focused like */
gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
+38 -1
View File
@@ -49,6 +49,7 @@
#include "chanlist.h"
#include "joind.h"
#include "xtext.h"
#include "inline-image.h"
#include "theme/theme-gtk.h"
#include "menu.h"
#include "notifygui.h"
@@ -89,6 +90,9 @@ static const GOptionEntry gopt_entries[] =
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
#ifdef __linux__
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
#endif
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"},
@@ -102,6 +106,33 @@ static const GOptionEntry gopt_entries[] =
{NULL}
};
#ifdef __linux__
void
fe_root_warning (void)
{
const char *message =
"ZoiteChat refuses to run as root on Linux.\n\n"
"Run it as a normal user, or pass --run-as-root to override this protection.";
GtkWidget *dialog;
g_printerr ("%s\n", message);
/* A graphical session may not be accessible to root, particularly under
* Wayland. Keep stderr as the fallback if GTK cannot open the display. */
if (!gtk_init_check (NULL, NULL))
return;
dialog = gtk_message_dialog_new (NULL,
GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
"%s", message);
gtk_window_set_title (GTK_WINDOW (dialog), "ZoiteChat");
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
}
#endif
#ifdef WIN32
static void
create_msg_dialog (gchar *title, gchar *message)
@@ -943,7 +974,13 @@ void
fe_print_text (struct session *sess, char *text, time_t stamp,
gboolean no_activity)
{
PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
/* image links become a "[view image]" placeholder with a hidden URL */
char *filtered = inline_image_filter_text (text);
PrintTextRaw (sess->res->buffer,
(unsigned char *)(filtered ? filtered : text),
prefs.hex_text_indent, stamp);
g_free (filtered);
if (no_activity || !sess->gui->is_tab)
return;
+2
View File
@@ -59,6 +59,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="fkeys.h" />
<ClInclude Include="gtkutil.h" />
<ClInclude Include="icon-resolver.h" />
<ClInclude Include="inline-image.h" />
<ClInclude Include="joind.h" />
<ClInclude Include="maingui.h" />
<ClInclude Include="preferences-persistence.h" />
@@ -104,6 +105,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClCompile Include="gtkutil.c" />
<ClCompile Include="icon-resolver.c" />
<ClCompile Include="ignoregui.c" />
<ClCompile Include="inline-image.c" />
<ClCompile Include="joind.c" />
<ClCompile Include="maingui.c" />
<ClCompile Include="menu.c" />
+6
View File
@@ -45,6 +45,9 @@
<ClInclude Include="icon-resolver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="inline-image.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="joind.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -170,6 +173,9 @@
<ClCompile Include="ignoregui.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="inline-image.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="joind.c">
<Filter>Source Files</Filter>
</ClCompile>
+13 -3
View File
@@ -1221,7 +1221,7 @@ key_dialog_load (GtkListStore *store)
void
key_dialog_show ()
{
GtkWidget *vbox, *box;
GtkWidget *vbox, *box, *pane, *scroll;
GtkWidget *view, *xtext, *delete_button;
GtkListStore *store;
XTextColor xtext_palette[XTEXT_COLS];
@@ -1237,10 +1237,20 @@ key_dialog_show ()
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
NULL, 600, 360, &vbox, 0);
view = key_dialog_treeview_new (vbox);
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
view = key_dialog_treeview_new (box);
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
gtk_paned_set_position (GTK_PANED (pane), 220);
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
xtext = gtk_xtext_new (xtext_palette, 0);
gtk_box_pack_start (GTK_BOX (vbox), xtext, FALSE, TRUE, 2);
gtk_container_add (GTK_CONTAINER (scroll), xtext);
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
g_object_set_data (G_OBJECT (key_dialog), "view", view);
+17 -11
View File
@@ -641,9 +641,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
g_object_set_data (G_OBJECT (dialog), "cb", callback);
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
@@ -653,7 +652,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
G_CALLBACK (gtkutil_str_enter), dialog);
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
label = gtk_label_new (msg);
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
@@ -661,7 +661,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (gtkutil_get_str_response), entry);
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
hbox, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
}
@@ -742,9 +743,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
g_object_set_data (G_OBJECT (dialog), "cb", callback);
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
@@ -755,7 +755,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
gtk_adjustment_set_upper (adj, 1024);
gtk_adjustment_set_step_increment (adj, 1);
gtk_spin_button_set_value ((GtkSpinButton*)spin, def);
gtk_box_pack_end (GTK_BOX (hbox), spin, 0, 0, 0);
gtk_widget_set_valign (spin, GTK_ALIGN_CENTER);
gtk_box_pack_end (GTK_BOX (hbox), spin, TRUE, TRUE, 0);
label = gtk_label_new (msg);
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
@@ -763,7 +764,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (gtkutil_get_number_response), spin);
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
hbox, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
}
@@ -783,17 +785,18 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
g_object_set_data (G_OBJECT (dialog), "cb", callback);
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
prompt_label = gtk_label_new (prompt);
gtk_label_set_line_wrap (GTK_LABEL (prompt_label), TRUE);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (gtkutil_get_bool_response), NULL);
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), prompt_label);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
prompt_label, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
}
@@ -885,7 +888,10 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
#endif
gtk_window_set_title (GTK_WINDOW (win), title);
gtk_window_set_default_size (GTK_WINDOW (win), width, height);
/* Zero means content-sized to callers; GTK requires -1 for that. */
gtk_window_set_default_size (GTK_WINDOW (win),
width > 0 ? width : -1,
height > 0 ? height : -1);
gtk_window_set_role (GTK_WINDOW (win), role);
if (flags & 1)
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
/* ZoiteChat
* Copyright (C) 2026 ZoiteChat contributors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifndef ZOITECHAT_INLINE_IMAGE_H
#define ZOITECHAT_INLINE_IMAGE_H
#include "xtext.h"
/* does this look like a direct http(s) link to an image file? */
gboolean inline_image_is_image_url (const char *url);
/* show the image behind 'url' inline below 'ent', or hide it again if it is
* already shown. Errors are reported to 'sess'. */
void inline_image_toggle (session *sess, GtkXText *xtext, textentry *ent,
const char *url);
/* replace image links in a message with a clickable placeholder label that
* hides the URL. Returns a newly allocated string, or NULL if the text has
* no image links (or the feature is off). */
char *inline_image_filter_text (const char *text);
/* pop up a window with a larger version of the entry's inline image */
void inline_image_show_viewer (GtkXText *xtext, textentry *ent);
/* can downloads go through the proxy configured for ZoiteChat (or is no
* proxy configured)? FALSE means loading media would have to bypass the
* user's proxy, which is never done. */
gboolean inline_image_proxy_usable (void);
#endif
-1
View File
@@ -137,7 +137,6 @@ joind_show_dialog (server *serv)
gtk_window_set_position (GTK_WINDOW (dialog1), GTK_WIN_POS_CENTER_ON_PARENT);
gtk_window_set_transient_for (GTK_WINDOW(dialog1), GTK_WINDOW(serv->front_session->gui->window));
gtk_window_set_modal (GTK_WINDOW (dialog1), TRUE);
gtk_window_set_resizable (GTK_WINDOW (dialog1), FALSE);
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog1));
gtk_widget_show (dialog_vbox1);
+292 -18
View File
@@ -61,6 +61,7 @@
#include "pixmaps.h"
#include "plugin-tray.h"
#include "xtext.h"
#include "inline-image.h"
#include "sexy-spell-entry.h"
#include "gtkutil.h"
@@ -777,10 +778,13 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui)
}
#define TYPING_THROTTLE_USEC (3 * G_USEC_PER_SEC)
static gboolean
mg_client_tag_allowed (server *serv, const char *tag)
{
char **deny;
gboolean blocked = FALSE;
int i;
if (!serv->have_message_tags)
@@ -792,30 +796,42 @@ mg_client_tag_allowed (server *serv, const char *tag)
deny = g_strsplit (serv->clienttagdeny, ",", 0);
for (i = 0; deny[i]; i++)
{
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
{
g_strfreev (deny);
return FALSE;
}
if (!strcmp (deny[i], "*"))
blocked = TRUE;
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
blocked = FALSE;
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
blocked = TRUE;
}
g_strfreev (deny);
return TRUE;
return !blocked;
}
static void
static gboolean
mg_typing_can_send (session *sess)
{
gint64 now = g_get_monotonic_time ();
return sess->typing_last_sent == 0 ||
now - sess->typing_last_sent >= TYPING_THROTTLE_USEC;
}
static gboolean
mg_send_typing (session *sess, const char *state)
{
char tags[32];
if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0])
return;
return FALSE;
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
return;
return FALSE;
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
sess->server->p_tagmsg (sess->server, tags, sess->channel);
sess->typing_last_sent = g_get_monotonic_time ();
return TRUE;
}
static int
@@ -844,17 +860,15 @@ mg_typing_update (session *sess, const char *text)
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
{
if (sess->typing_status)
if (sess->typing_status && mg_typing_can_send (sess))
mg_send_typing (sess, "done");
sess->typing_status = 0;
return;
}
if (sess->typing_status != 1)
{
if (mg_typing_can_send (sess))
mg_send_typing (sess, "active");
sess->typing_status = 1;
}
sess->typing_status = 1;
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
}
@@ -926,10 +940,236 @@ mg_send_reply_or_text (session *sess, char *cmd)
mg_reply_update (sess);
}
typedef struct
{
gboolean bold;
gboolean italic;
gboolean underline;
gboolean strikethrough;
gboolean reverse;
gint foreground;
gint background;
gboolean pending_foreground;
gboolean pending_background;
} MgInputFormatState;
static void
mg_input_format_state_at_cursor (GtkEntry *entry, MgInputFormatState *state)
{
const char *text;
const char *cursor;
gsize limit;
gsize i;
gint cursor_pos;
memset (state, 0, sizeof (*state));
state->foreground = -1;
state->background = -1;
text = gtk_entry_get_text (entry);
cursor_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
if (!text || cursor_pos < 0)
return;
cursor = g_utf8_offset_to_pointer (text, cursor_pos);
limit = (gsize) (cursor - text);
for (i = 0; i < limit;)
{
guchar ch = (guchar) text[i++];
switch (ch)
{
case ATTR_BOLD:
state->bold = !state->bold;
break;
case ATTR_ITALICS:
state->italic = !state->italic;
break;
case ATTR_UNDERLINE:
state->underline = !state->underline;
break;
case ATTR_STRIKETHROUGH:
state->strikethrough = !state->strikethrough;
break;
case ATTR_REVERSE:
state->reverse = !state->reverse;
break;
case ATTR_RESET:
case '\n':
state->bold = FALSE;
state->italic = FALSE;
state->underline = FALSE;
state->strikethrough = FALSE;
state->reverse = FALSE;
state->foreground = -1;
state->background = -1;
state->pending_foreground = FALSE;
state->pending_background = FALSE;
break;
case ATTR_COLOR:
{
gint value = 0;
gint digits = 0;
state->pending_foreground = FALSE;
state->pending_background = FALSE;
if (i >= limit)
{
state->pending_foreground = TRUE;
break;
}
if (!g_ascii_isdigit (text[i]))
{
state->foreground = -1;
state->background = -1;
break;
}
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
{
value = (value * 10) + (text[i] - '0');
i++;
digits++;
}
state->foreground = value;
state->background = -1;
if (i < limit && text[i] == ',')
{
i++;
if (i >= limit)
{
state->pending_background = TRUE;
break;
}
value = 0;
digits = 0;
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
{
value = (value * 10) + (text[i] - '0');
i++;
digits++;
}
if (digits > 0)
state->background = value;
}
}
break;
default:
break;
}
}
}
static void
mg_input_format_append (GString *summary, const char *text)
{
if (summary->len > 0)
g_string_append (summary, " · ");
g_string_append (summary, text);
}
static void
mg_input_format_color_value (char *buffer, gsize buffer_len, const char *label, gint color_index)
{
guint16 red;
guint16 green;
guint16 blue;
if (theme_get_mirc_color_rgb16 ((unsigned int) color_index, &red, &green, &blue))
{
g_snprintf (buffer, buffer_len,
"%s <b>%02d</b> <span foreground=\"#%02x%02x%02x\">■</span>",
label, color_index, (guint) (red >> 8), (guint) (green >> 8),
(guint) (blue >> 8));
return;
}
g_snprintf (buffer, buffer_len, "%s <b>%02d</b>", label, color_index);
}
static void
mg_input_format_update (GtkEntry *entry)
{
GtkWidget *format_box;
GtkWidget *format_label;
MgInputFormatState state;
GString *summary;
char color[96];
char *markup;
format_box = g_object_get_data (G_OBJECT (entry), "zoitechat-format-box");
format_label = g_object_get_data (G_OBJECT (entry), "zoitechat-format-label");
if (!format_box || !format_label)
return;
if (!prefs.hex_gui_input_attr)
{
gtk_widget_hide (format_box);
return;
}
mg_input_format_state_at_cursor (entry, &state);
summary = g_string_new (NULL);
if (state.bold)
mg_input_format_append (summary, "<b>Bold</b>");
if (state.italic)
mg_input_format_append (summary, "<i>Italic</i>");
if (state.underline)
mg_input_format_append (summary, "<u>Underline</u>");
if (state.strikethrough)
mg_input_format_append (summary, "<s>Strikethrough</s>");
if (state.reverse)
mg_input_format_append (summary, "Reverse");
if (state.foreground >= 0)
{
mg_input_format_color_value (color, sizeof (color), "Text color", state.foreground);
mg_input_format_append (summary, color);
}
if (state.background >= 0)
{
mg_input_format_color_value (color, sizeof (color), "Background", state.background);
mg_input_format_append (summary, color);
}
if (state.pending_foreground)
mg_input_format_append (summary, "Color: type foreground[,background]");
else if (state.pending_background)
mg_input_format_append (summary, "Background: type color");
if (summary->len == 0)
{
gtk_widget_hide (format_box);
g_string_free (summary, TRUE);
return;
}
markup = g_strdup_printf ("<span foreground='#7d8790'>Formatting · %s</span>", summary->str);
gtk_label_set_markup (GTK_LABEL (format_label), markup);
gtk_widget_show (format_label);
gtk_widget_show (format_box);
g_free (markup);
g_string_free (summary, TRUE);
}
static void
mg_inputbox_cursor_changed (GObject *object, GParamSpec *pspec, gpointer userdata)
{
(void) pspec;
(void) userdata;
mg_input_format_update (GTK_ENTRY (object));
}
static void
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
{
key_check_replace_on_change (editable, NULL);
mg_input_format_update (GTK_ENTRY (editable));
if (current_sess && current_sess->gui == gui)
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
}
@@ -2090,7 +2330,6 @@ mg_open_quit_dialog (gboolean minimize_button)
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
gtk_widget_show (dialog_vbox1);
@@ -3692,7 +3931,13 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
{
if (word == NULL)
{
mg_focus (sess);
/* a click on a shown inline image opens the viewer */
textentry *img_ent = gtk_xtext_get_clicked_image_entry (GTK_XTEXT (xtext));
if (img_ent)
inline_image_show_viewer (GTK_XTEXT (xtext), img_ent);
else
mg_focus (sess);
return;
}
@@ -3704,6 +3949,20 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
case WORD_HOST6:
case WORD_HOST:
word[end] = 0;
/* image links toggle an inline preview instead
of opening the browser */
if (word_type == WORD_URL &&
prefs.hex_net_remote_media &&
inline_image_is_image_url (word + start))
{
textentry *ent = gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext));
if (ent)
{
inline_image_toggle (sess, GTK_XTEXT (xtext), ent, word + start);
break;
}
}
fe_open_url (word + start);
}
}
@@ -3732,7 +3991,8 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
case WORD_HOST:
word[end] = 0;
word += start;
menu_urlmenu (even, word);
menu_urlmenu_entry (even, word, xtext,
gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext)));
break;
case WORD_NICK:
word[end] = 0;
@@ -4679,9 +4939,19 @@ mg_create_search(session *sess, GtkWidget *box)
static void
mg_create_entry (session *sess, GtkWidget *box)
{
GtkWidget *hbox, *but, *entry;
GtkWidget *hbox, *but, *entry, *format_box, *format_label;
session_gui *gui = sess->gui;
format_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_widget_set_name (format_box, "zoitechat-formatbar");
gtk_widget_set_no_show_all (format_box, TRUE);
gtk_box_pack_start (GTK_BOX (box), format_box, 0, 0, 0);
format_label = gtk_label_new ("");
gtk_label_set_ellipsize (GTK_LABEL (format_label), PANGO_ELLIPSIZE_END);
gtk_box_pack_start (GTK_BOX (format_box), format_label, TRUE, TRUE, 8);
gtk_widget_show (format_label);
gtk_widget_hide (format_box);
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
gtk_widget_set_no_show_all (gui->reply_box, TRUE);
@@ -4712,12 +4982,16 @@ mg_create_entry (session *sess, GtkWidget *box)
gui->input_box = entry = sexy_spell_entry_new ();
sexy_spell_entry_set_checked ((SexySpellEntry *)entry, prefs.hex_gui_input_spell);
sexy_spell_entry_set_parse_attributes ((SexySpellEntry *)entry, prefs.hex_gui_input_attr);
g_object_set_data (G_OBJECT (entry), "zoitechat-format-box", format_box);
g_object_set_data (G_OBJECT (entry), "zoitechat-format-label", format_label);
gtk_entry_set_max_length (GTK_ENTRY (gui->input_box), 0);
g_signal_connect (G_OBJECT (entry), "activate",
G_CALLBACK (mg_inputbox_cb), gui);
g_signal_connect (G_OBJECT (entry), "changed",
G_CALLBACK (mg_inputbox_changed), gui);
g_signal_connect (G_OBJECT (entry), "notify::cursor-position",
G_CALLBACK (mg_inputbox_cursor_changed), gui);
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
gtk_widget_set_name (entry, "zoitechat-inputbox");
+61 -5
View File
@@ -45,6 +45,7 @@
#include "../common/util.h"
#include "../common/text.h"
#include "xtext.h"
#include "inline-image.h"
#include "ascii.h"
#include "banlist.h"
#include "chanlist.h"
@@ -1056,8 +1057,40 @@ open_url_cb (GtkWidget *item, char *url)
handle_command (current_sess, buf, FALSE);
}
/* the text entry the open URL menu was created for; the widget pointer is
nulled automatically if the widget dies while the menu is up */
static GtkXText *urlmenu_xtext = NULL;
static textentry *urlmenu_ent = NULL;
static void
menu_urlmenu_set_entry (GtkWidget *xtext, textentry *ent)
{
if (urlmenu_xtext)
g_object_remove_weak_pointer (G_OBJECT (urlmenu_xtext),
(gpointer *) &urlmenu_xtext);
urlmenu_xtext = xtext ? GTK_XTEXT (xtext) : NULL;
urlmenu_ent = ent;
if (urlmenu_xtext)
g_object_add_weak_pointer (G_OBJECT (urlmenu_xtext),
(gpointer *) &urlmenu_xtext);
}
static void
inline_image_menu_cb (GtkWidget *item, char *url)
{
if (urlmenu_xtext == NULL || urlmenu_ent == NULL)
return;
/* the line may have scrolled out of the buffer since the menu opened */
if (!gtk_xtext_buffer_contains (urlmenu_xtext, urlmenu_ent))
return;
inline_image_toggle (current_sess, urlmenu_xtext, urlmenu_ent, url);
}
void
menu_urlmenu (GdkEventButton *event, char *url)
menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
textentry *ent)
{
GtkWidget *menu;
char *tmp, *chop;
@@ -1065,6 +1098,14 @@ menu_urlmenu (GdkEventButton *event, char *url)
g_free (str_copy);
str_copy = g_strdup (url);
if (xtext == NULL || ent == NULL || !prefs.hex_net_remote_media ||
!inline_image_is_image_url (str_copy))
{
xtext = NULL;
ent = NULL;
}
menu_urlmenu_set_entry (xtext, ent);
menu = menu_new ();
/* more than 51 chars? Chop it */
if (g_utf8_strlen (str_copy, -1) >= 52)
@@ -1087,6 +1128,15 @@ menu_urlmenu (GdkEventButton *event, char *url)
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
else
menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy);
if (ent)
{
const char *shown = gtk_xtext_entry_get_image_url (GTK_XTEXT (xtext), ent);
menu_quick_item_with_callback (inline_image_menu_cb,
shown && strcmp (shown, str_copy) == 0 ?
_("Hide Inline Image") : _("Show Image Inline"),
menu, str_copy);
}
menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy);
/* custom ones from urlhandlers.conf */
menu_create (menu, urlhandler_list, str_copy, TRUE);
@@ -1094,6 +1144,12 @@ menu_urlmenu (GdkEventButton *event, char *url)
menu_popup (menu, event, NULL);
}
void
menu_urlmenu (GdkEventButton *event, char *url)
{
menu_urlmenu_entry (event, url, NULL, NULL);
}
static void
menu_chan_cycle (GtkWidget * menu, char *chan)
{
@@ -1539,16 +1595,16 @@ menu_join (GtkWidget * wid, gpointer none)
NULL);
theme_manager_attach_window (dialog);
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
gtk_box_set_homogeneous (GTK_BOX (content_area), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
entry = gtk_entry_new ();
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE); /* avoid auto-selection */
gtk_entry_set_text (GTK_ENTRY (entry), "#");
g_signal_connect (G_OBJECT (entry), "activate",
G_CALLBACK (menu_join_entry_cb), dialog);
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
label = gtk_label_new (_("Enter Channel to Join:"));
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
@@ -1556,7 +1612,7 @@ menu_join (GtkWidget * wid, gpointer none)
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (menu_join_cb), entry);
gtk_container_add (GTK_CONTAINER (content_area), hbox);
gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
+3
View File
@@ -20,8 +20,11 @@
#ifndef ZOITECHAT_MENU_H
#define ZOITECHAT_MENU_H
#include "xtext.h"
GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets);
void menu_urlmenu (GdkEventButton * event, char *url);
void menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext, textentry *ent);
void menu_chanmenu (session *sess, GdkEventButton * event, char *chan);
void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
void menu_addconnectmenu (server *serv, GtkWidget *menu);
+3 -2
View File
@@ -25,6 +25,7 @@ zoitechat_gtk_sources = [
'gtkutil.c',
'icon-resolver.c',
'ignoregui.c',
'inline-image.c',
'joind.c',
'menu.c',
'maingui.c',
@@ -110,7 +111,7 @@ zoitechat_theme_lib = static_library('zoitechat_theme',
iso_codes = dependency('iso-codes', required: false)
if iso_codes.found()
zoitechat_gtk_sources += 'sexy-iso-codes.c'
iso_codes_prefix = iso_codes.get_pkgconfig_variable('prefix')
iso_codes_prefix = iso_codes.get_variable(pkgconfig : 'prefix')
zoitechat_gtk_cflags += '-DISO_CODES_PREFIX="@0@"'.format(iso_codes_prefix)
zoitechat_gtk_cflags += '-DISO_CODES_LOCALEDIR="@0@"'.format(
join_paths(iso_codes_prefix, 'share/locale'))
@@ -139,7 +140,7 @@ executable('zoitechat',
link_args: zoitechat_gtk_ldflags,
pie: true,
install: true,
gui_app: true,
win_subsystem: 'windows',
)
emoji_ui_tests = executable('emoji_ui_tests',
+11 -6
View File
@@ -391,34 +391,39 @@ fe_notify_ask (char *nick, char *networks)
gtk_container_set_border_width (GTK_CONTAINER (table), 12);
gtk_grid_set_row_spacing (GTK_GRID (table), 3);
gtk_grid_set_column_spacing (GTK_GRID (table), 8);
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), table);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
table, TRUE, TRUE, 0);
label = gtk_label_new (msg);
gtkutil_grid_attach_defaults (table, label, 0, 1, 0, 1);
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
gtkutil_grid_attach (table, label, 0, 1, 0, 1, GTKUTIL_ATTACH_FILL, 0, 0, 0);
entry = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry), nick);
g_signal_connect (G_OBJECT (entry), "activate",
G_CALLBACK (notifygui_add_enter), dialog);
gtkutil_grid_attach_defaults (table, entry, 1, 2, 0, 1);
gtkutil_grid_attach (table, entry, 1, 2, 0, 1,
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (notifygui_add_cb), entry);
label = gtk_label_new (_("Notify on these networks:"));
gtkutil_grid_attach_defaults (table, label, 0, 1, 2, 3);
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
gtkutil_grid_attach (table, label, 0, 1, 2, 3, GTKUTIL_ATTACH_FILL, 0, 0, 0);
wid = gtk_entry_new ();
g_object_set_data (G_OBJECT (entry), "net", wid);
g_signal_connect (G_OBJECT (wid), "activate",
G_CALLBACK (notifygui_add_enter), dialog);
gtk_entry_set_text (GTK_ENTRY (wid), networks ? networks : "ALL");
gtkutil_grid_attach_defaults (table, wid, 1, 2, 2, 3);
gtkutil_grid_attach (table, wid, 1, 2, 2, 3,
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
label = gtk_label_new (NULL);
g_snprintf (buf, sizeof (buf), "<i><span size=\"smaller\">%s</span></i>", _("Comma separated list of networks is accepted."));
gtk_label_set_markup (GTK_LABEL (label), buf);
gtkutil_grid_attach_defaults (table, label, 1, 2, 3, 4);
gtkutil_grid_attach (table, label, 1, 2, 3, 4, GTKUTIL_ATTACH_FILL, 0, 0, 0);
gtk_widget_show_all (dialog);
}
+20 -4
View File
@@ -2423,6 +2423,8 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
GtkWidget *comboboxentry_charset;
GtkWidget *combobox_logintypes;
GtkWidget *hbox1;
GtkWidget *content_scroll;
GtkWidget *content_box;
GtkWidget *scrolledwindow2;
GtkWidget *scrolledwindow4;
GtkWidget *scrolledwindow5;
@@ -2449,7 +2451,9 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
gtk_window_set_default_size (GTK_WINDOW (editwindow), netedit_win_width, netedit_win_height);
gtk_window_set_default_size (GTK_WINDOW (editwindow),
netedit_win_width > 0 ? netedit_win_width : -1,
netedit_win_height > 0 ? netedit_win_height : -1);
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
@@ -2458,10 +2462,20 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
/* Keep Close accessible on small screens, while the lists still receive
all spare space when the complete form fits in the window. */
content_scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (content_scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
gtk_box_pack_start (GTK_BOX (vbox5), content_scroll, TRUE, TRUE, 0);
content_box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_container_add (GTK_CONTAINER (content_scroll), content_box);
/* Tabs and buttons */
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox5), hbox1, TRUE, TRUE, 4);
gtk_box_pack_start (GTK_BOX (content_box), hbox1, TRUE, TRUE, 4);
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
@@ -2599,7 +2613,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
/* Checkboxes and entries */
table3 = gtkutil_grid_new (19, 2, FALSE);
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (content_box), table3, FALSE, FALSE, 0);
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
@@ -2828,7 +2842,9 @@ servlist_open_networks (void)
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (servlist), buf);
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
gtk_window_set_default_size (GTK_WINDOW (servlist),
netlist_win_width > 0 ? netlist_win_width : -1,
netlist_win_height > 0 ? netlist_win_height : -1);
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
if (current_sess)
+25 -8
View File
@@ -340,6 +340,9 @@ static const setting tabs_settings[] =
{ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0},
{ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0},
{ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0},
{ST_EFONT, N_("Tab font:"), P_OFFSETNL(hex_gui_tab_font),
N_("Font for server and channel tabs in the Tabs layout. Leave blank to use the theme font. Smaller text still applies."),
0, sizeof prefs.hex_gui_tab_font},
{ST_MENU, N_("Focus new tabs:"), P_OFFINTNL(hex_gui_tab_newtofront), 0, focusnewtabsmenu, 0},
{ST_MENU, N_("Placement of notices:"), P_OFFINTNL(hex_irc_notice_pos), 0, noticeposmenu, 0},
{ST_MENU, N_("Show channel switcher at:"), P_OFFINTNL(hex_gui_tab_pos), 0, cspos, 1},
@@ -674,6 +677,11 @@ static const setting network_settings[] =
{ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user},
{ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass},
{ST_HEADER, N_("Remote Media"), 0, 0, 0, 0},
{ST_TOGGLE, N_("Allow loading remote media (inline images) when clicked"),
P_OFFINTNL(hex_net_remote_media),
N_("Loading remote media contacts the site hosting it, which can reveal your IP address and that you viewed the message. You are asked for confirmation before each new domain is contacted."), 0, 0},
{ST_END, 0, 0, 0, 0, 0}
};
@@ -1248,7 +1256,7 @@ setup_create_id_menu (GtkWidget *table, char *label, int row, char *dest)
static void
setup_create_menu (GtkWidget *table, int row, const setting *set)
{
GtkWidget *wid, *cbox, *box;
GtkWidget *wid, *cbox;
const char **text = (const char **)set->list;
int i;
@@ -1269,9 +1277,7 @@ setup_create_menu (GtkWidget *table, int row, const setting *set)
g_signal_connect (G_OBJECT (cbox), "changed",
G_CALLBACK (setup_menu_cb), (gpointer)set);
box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
gtk_box_pack_start (GTK_BOX (box), cbox, 0, 0, 0);
setup_table_attach (table, box, 3, 4, row, row + 1, TRUE, FALSE,
setup_table_attach (table, cbox, 3, 6, row, row + 1, TRUE, FALSE,
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
}
@@ -1955,7 +1961,8 @@ setup_add_page (const char *title, GtkWidget *book, GtkWidget *tab)
sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL));
gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (sw, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_policy (sw, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_propagate_natural_width (sw, TRUE);
gtk_container_add (GTK_CONTAINER (sw), vvbox);
viewport = gtk_bin_get_child (GTK_BIN(sw));
@@ -1999,7 +2006,9 @@ setup_ensure_page_created (int page)
}
page_widget = setup_page_factories[page] (setup_page_settings[page]);
gtk_container_add (GTK_CONTAINER (container), page_widget);
/* Lazy pages must pass the notebook's spare space to their content,
particularly the independently scrolling Sounds list. */
gtk_box_pack_start (GTK_BOX (container), page_widget, TRUE, TRUE, 0);
gtk_widget_show_all (container);
}
@@ -2177,9 +2186,14 @@ setup_create_tree (GtkWidget *box, GtkWidget *book)
-1, _("Categories"), renderer, "text", 0, NULL);
gtk_tree_view_expand_all (GTK_TREE_VIEW (tree));
frame = gtk_frame_new (NULL);
frame = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (frame),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (frame), GTK_SHADOW_IN);
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (frame), TRUE);
gtk_widget_set_vexpand (frame, TRUE);
gtk_container_add (GTK_CONTAINER (frame), tree);
gtk_box_pack_start (GTK_BOX (box), frame, 0, 0, 0);
gtk_box_pack_start (GTK_BOX (box), frame, FALSE, TRUE, 0);
gtk_box_reorder_child (GTK_BOX (box), frame, 0);
if (sel_iter)
@@ -2330,6 +2344,9 @@ setup_apply (struct zoitechatprefs *pr)
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
DIFF (hex_gui_tab_small))
live_changes.chanview = TRUE;
if (pr->hex_gui_tab_layout == 0 &&
strcmp (pr->hex_gui_tab_font, prefs.hex_gui_tab_font) != 0)
live_changes.chanview = TRUE;
if (DIFF (hex_gui_tab_sort))
live_changes.tab_resort = TRUE;
if (DIFF (hex_gui_tab_trunc))
+2 -11
View File
@@ -1266,8 +1266,6 @@ attr_list_has_attrs (PangoAttrList *attrs)
static void
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
{
GdkRectangle rect;
GtkAllocation allocation;
GtkWidget *widget = GTK_WIDGET(entry);
int length, i, text_len;
const char *text;
@@ -1299,15 +1297,8 @@ sexy_spell_entry_recheck_all(SexySpellEntry *entry)
gtk_entry_set_attributes (GTK_ENTRY (entry), attr_list_has_attrs (entry->priv->attr_list) ? entry->priv->attr_list : NULL);
if (gtk_widget_get_realized (GTK_WIDGET(entry)))
{
gtk_widget_get_allocation (GTK_WIDGET(entry), &allocation);
rect.x = 0; rect.y = 0;
rect.width = allocation.width;
rect.height = allocation.height;
gdk_window_invalidate_rect(gtk_widget_get_window (widget), &rect, TRUE);
}
/* GtkEntry shares its parent's GdkWindow in GTK3; redraw the widget area. */
gtk_widget_queue_draw (widget);
}
static gboolean
+10 -7
View File
@@ -400,7 +400,6 @@ pevent_treeview_new (GtkWidget *box)
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
gtk_widget_set_size_request (GTK_WIDGET (scroll), -1, 250);
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
g_return_val_if_fail (store != NULL, NULL);
@@ -437,7 +436,7 @@ pevent_treeview_new (GtkWidget *box)
gtk_tree_view_column_set_min_width (col, 100);
gtk_container_add (GTK_CONTAINER (scroll), view);
gtk_container_add (GTK_CONTAINER (box), scroll);
gtk_paned_pack1 (GTK_PANED (box), scroll, TRUE, FALSE);
return view;
}
@@ -481,7 +480,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
gtk_container_add (GTK_CONTAINER (scroll), view);
gtk_container_add (GTK_CONTAINER (box), scroll);
gtk_paned_pack2 (GTK_PANED (box), scroll, TRUE, FALSE);
return view;
}
@@ -489,7 +488,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
void
pevent_dialog_show ()
{
GtkWidget *vbox, *hbox, *wid, *pane;
GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane;
XTextColor xtext_palette[XTEXT_COLS];
if (pevent_dialog)
@@ -503,8 +502,12 @@ pevent_dialog_show ()
TRUE, FALSE, pevent_dialog_close, NULL,
600, 455, &vbox, 0);
preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0);
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
gtk_paned_pack1 (GTK_PANED (preview_pane), pane, TRUE, FALSE);
gtk_paned_set_position (GTK_PANED (pane), 220);
gtk_paned_set_position (GTK_PANED (preview_pane), 330);
pevent_dialog_list = pevent_treeview_new (pane);
pevent_dialog_fill (pevent_dialog_list);
@@ -513,12 +516,12 @@ pevent_dialog_show ()
wid = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (wid), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
gtk_box_pack_start (GTK_BOX (vbox), wid, FALSE, TRUE, 0);
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (wid), 75);
gtk_paned_pack2 (GTK_PANED (preview_pane), wid, TRUE, FALSE);
theme_get_xtext_colors_for_widget (wid, xtext_palette, XTEXT_COLS);
pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0);
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
gtk_widget_set_size_request (pevent_dialog_twid, -1, 75);
gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid);
gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font);
g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid);
+1 -1
View File
@@ -683,7 +683,7 @@ theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_de
behavior.font_desc = font_desc;
behavior.apply_background = TRUE;
behavior.apply_foreground = (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks) ? FALSE : TRUE;
behavior.apply_foreground = prefs.hex_gui_ulist_color ? FALSE : TRUE;
return behavior;
}
+1
View File
@@ -1911,6 +1911,7 @@ theme_preferences_create_page (GtkWindow *parent,
G_TYPE_STRING,
G_TYPE_INT);
ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (gtk3_store));
gtk_widget_set_hexpand (ui->gtk3_combo, TRUE);
g_object_unref (gtk3_store);
renderer = gtk_cell_renderer_text_new ();
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, TRUE);
+2 -2
View File
@@ -594,7 +594,7 @@ fe_userlist_rehash (session *sess, struct User *user)
nick_token = THEME_TOKEN_TAB_AWAY;
have_nick_token = TRUE;
}
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
else if (prefs.hex_gui_ulist_color)
{
int mirc_index = text_color_of (user->nick);
@@ -635,7 +635,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
nick_token = THEME_TOKEN_TAB_AWAY;
have_nick_token = TRUE;
}
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
else if (prefs.hex_gui_ulist_color)
{
int mirc_index = text_color_of (newuser->nick);
+599 -20
View File
@@ -93,8 +93,104 @@ struct textentry
guchar pad1;
guchar pad2;
GList *marks; /* List of found strings */
GdkPixbuf *image; /* inline image shown below the text, or NULL */
GdkPixbuf *image_full; /* larger version for the image viewer */
char *image_url; /* the URL the image was loaded from */
};
/* inline image display */
#define XTEXT_IMAGE_PADDING 4
#define XTEXT_IMAGE_MAX_WIDTH 480
#define XTEXT_IMAGE_MAX_HEIGHT 360
#define XTEXT_IMAGE_FULL_MAX_WIDTH 1600
#define XTEXT_IMAGE_FULL_MAX_HEIGHT 1200
/* an inline image load that has been started but not yet completed;
ent/buf/xtext are invalidated (entry removed) when any of them dies */
typedef struct
{
guint id;
GtkXText *xtext;
xtext_buffer *buf;
textentry *ent;
char *url;
} xtext_image_pending;
static GSList *image_pending_list = NULL;
static guint image_pending_next_id = 1;
static void
gtk_xtext_image_pending_forget (gboolean (*match) (xtext_image_pending *, gconstpointer), gconstpointer data)
{
GSList *list = image_pending_list;
while (list)
{
GSList *next = list->next;
xtext_image_pending *pending = list->data;
if (match (pending, data))
{
image_pending_list = g_slist_delete_link (image_pending_list, list);
g_free (pending->url);
g_free (pending);
}
list = next;
}
}
static gboolean
gtk_xtext_image_pending_match_ent (xtext_image_pending *pending, gconstpointer ent)
{
return pending->ent == ent;
}
static gboolean
gtk_xtext_image_pending_match_buf (xtext_image_pending *pending, gconstpointer buf)
{
return pending->buf == buf;
}
static gboolean
gtk_xtext_image_pending_match_xtext (xtext_image_pending *pending, gconstpointer xtext)
{
return pending->xtext == xtext;
}
/* number of extra screen lines an entry needs for its inline image */
static int
gtk_xtext_image_rows (GtkXText *xtext, textentry *ent)
{
if (ent->image == NULL || xtext->fontsize <= 0)
return 0;
return (gdk_pixbuf_get_height (ent->image) + XTEXT_IMAGE_PADDING * 2 +
xtext->fontsize - 1) / xtext->fontsize;
}
/* free an entry's inline image and drop any pending load aimed at it;
must be called whenever a textentry is freed */
static void
gtk_xtext_ent_free_image (textentry *ent)
{
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_ent, ent);
if (ent->image)
{
g_object_unref (ent->image);
ent->image = NULL;
}
if (ent->image_full)
{
g_object_unref (ent->image_full);
ent->image_full = NULL;
}
g_free (ent->image_url);
ent->image_url = NULL;
}
enum
{
WORD_CLICK,
@@ -177,6 +273,8 @@ static void gtk_xtext_render_page (GtkXText * xtext);
static void gtk_xtext_calc_lines (xtext_buffer *buf, int);
static gboolean gtk_xtext_is_selecting (GtkXText *xtext);
static char *gtk_xtext_selection_get_text (GtkXText *xtext, int *len_ret);
static void gtk_xtext_clipboard_get (GtkClipboard *, GtkSelectionData *, guint, gpointer);
static void gtk_xtext_clipboard_clear (GtkClipboard *, gpointer);
static textentry *gtk_xtext_nth (GtkXText *xtext, int line, int *subline);
static void gtk_xtext_adjustment_changed (GtkAdjustment * adj,
GtkXText * xtext);
@@ -214,6 +312,7 @@ static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk
static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp);
static gboolean gtk_xtext_word_select_char (const unsigned char *ch);
static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len);
static textentry * gtk_xtext_image_hit (GtkXText *xtext, int x, int y);
static inline void
gtk_xtext_cursor_unref (GdkCursor *cursor)
@@ -985,9 +1084,29 @@ gtk_xtext_new (const XTextColor *palette, int separator)
return GTK_WIDGET (xtext);
}
static void
gtk_xtext_release_selections (GtkXText *xtext)
{
GdkAtom selections[] = { GDK_SELECTION_PRIMARY, GDK_SELECTION_SECONDARY };
guint i;
if (!gtk_widget_has_screen (GTK_WIDGET (xtext)))
return;
for (i = 0; i < G_N_ELEMENTS (selections); i++)
{
GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (xtext), selections[i]);
if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (xtext))
gtk_clipboard_clear (clipboard);
}
}
static void
gtk_xtext_cleanup (GtkXText *xtext)
{
gtk_xtext_release_selections (xtext);
if (xtext->add_io_tag)
{
g_source_remove (xtext->add_io_tag);
@@ -1064,6 +1183,8 @@ gtk_xtext_dispose (GObject *object)
{
GtkXText *xtext = GTK_XTEXT (object);
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_xtext, xtext);
g_clear_pointer (&xtext->tooltip_url, g_free);
gtk_xtext_cleanup (xtext);
if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose)
@@ -1080,6 +1201,7 @@ gtk_xtext_finalize (GObject *object)
static void
gtk_xtext_unrealize (GtkWidget * widget)
{
gtk_xtext_release_selections (GTK_XTEXT (widget));
backend_deinit (GTK_XTEXT (widget));
/*
@@ -1445,7 +1567,7 @@ gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
str = ent->str + gtk_xtext_find_subline (xtext, ent, subline);
if (str >= ent->str + ent->str_len)
return 0;
return ent->str_len; /* e.g. the blank rows of an inline image */
/* Let user select left a few pixels to grab hidden text e.g. '<' */
if (x < indent - xtext->space_width)
@@ -2145,6 +2267,7 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
int len;
int out_of_bounds = 0;
int len_to_offset = 0;
gboolean has_hidden;
ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds);
if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len)
@@ -2189,10 +2312,15 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
if (ret_len)
*ret_len = len; /* Length before stripping */
/* a word with hidden text is a placeholder (e.g. an inline image link
label); its whole visible part acts as the link */
has_hidden = memchr (word, ATTR_HIDDEN, len) != NULL;
word = gtk_xtext_strip_color (word, len, xtext->scratch_buffer, NULL, slp, FALSE);
/* avoid turning the cursor into a hand for non-url part of the word */
if (xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
if (!has_hidden &&
xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
{
int start, end;
url_last (&start, &end);
@@ -2276,12 +2404,43 @@ gtk_xtext_unrender_hilight (GtkXText *xtext)
xtext->un_hilight = FALSE;
}
/* show (or stop showing) the destination URL of a hovered placeholder
word as the widget's tooltip */
static void
gtk_xtext_set_url_tooltip (GtkXText *xtext, const char *url, int url_len)
{
if (url == NULL)
{
if (xtext->tooltip_url)
{
g_free (xtext->tooltip_url);
xtext->tooltip_url = NULL;
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), NULL);
xtext->tooltip_stamp_set = FALSE;
}
return;
}
if (xtext->tooltip_url &&
strlen (xtext->tooltip_url) == (gsize) url_len &&
memcmp (xtext->tooltip_url, url, url_len) == 0)
return;
g_free (xtext->tooltip_url);
xtext->tooltip_url = g_strndup (url, url_len);
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), xtext->tooltip_url);
xtext->tooltip_stamp_set = FALSE;
}
static gboolean
gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event)
{
GtkXText *xtext = GTK_XTEXT (widget);
GdkWindow *window = gtk_widget_get_window (widget);
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
if (xtext->cursor_hand)
{
gtk_xtext_unrender_hilight (xtext);
@@ -2345,6 +2504,7 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
GSList *slp = NULL;
unsigned char *word;
int word_type = 0;
gboolean placeholder = FALSE;
word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp);
if (word)
@@ -2354,7 +2514,18 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word);
if (word_type > 0)
{
if (url_last (&laststart, &lastend))
/* words with hidden text are placeholders (e.g. inline image
links): hilight the whole word, not just the URL inside it */
if (memchr ((*word_ent)->str + *offset, ATTR_HIDDEN, *len))
{
/* the label hides the real destination, so show the full
URL as a tooltip while hovering */
placeholder = TRUE;
if (url_last (&laststart, &lastend) && lastend > laststart)
gtk_xtext_set_url_tooltip (xtext,
(const char *) word + laststart, lastend - laststart);
}
else if (url_last (&laststart, &lastend))
{
int cumlen, startadj = 0, endadj = 0;
offlen_t *meta;
@@ -2384,6 +2555,9 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
}
g_slist_free_full (slp, g_free);
if (!placeholder)
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
return word_type;
}
@@ -2515,6 +2689,20 @@ gtk_xtext_motion_notify (GtkWidget * widget, GdkEventMotion * event)
}
tooltip_check:
/* hovering an inline image? it opens the viewer when clicked */
if (gtk_xtext_image_hit (xtext, x, y))
{
if (xtext->hilight_ent) /* moving here straight from a link */
gtk_xtext_leave_notify (widget, NULL);
if (!xtext->cursor_hand)
{
gdk_window_set_cursor (window, xtext->hand_cursor);
xtext->cursor_hand = TRUE;
xtext->cursor_resize = FALSE;
}
return FALSE;
}
if (xtext->buffer->time_stamp && xtext->buffer->indent > 0 && x >= 0 && x < xtext->stamp_width)
{
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
@@ -2578,8 +2766,16 @@ gtk_xtext_set_clip_owner (GtkWidget * xtext, GdkEventButton * event)
{
gtk_clipboard_set_text (gtk_widget_get_clipboard (xtext, GDK_SELECTION_CLIPBOARD), str, len);
gtk_selection_owner_set (xtext, GDK_SELECTION_PRIMARY, event ? event->time : GDK_CURRENT_TIME);
gtk_selection_owner_set (xtext, GDK_SELECTION_SECONDARY, event ? event->time : GDK_CURRENT_TIME);
/* Let GtkClipboard own the X11 selection window. Making xtext
* the selection owner forces its window native and bypasses
* GTK's toplevel backing buffer on subsequent redraws.
*/
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_PRIMARY),
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_SECONDARY),
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
}
g_free (str);
@@ -2622,6 +2818,46 @@ gtk_xtext_unselect (GtkXText *xtext)
xtext->buffer->last_ent_end = NULL;
}
/* check whether the given widget coordinates hit an entry's inline image;
returns the entry if so */
static textentry *
gtk_xtext_image_hit (GtkXText *xtext, int x, int y)
{
textentry *ent;
int line, subline, image_rows, text_rows, rel_y, img_x;
if (xtext->fontsize <= 0)
return NULL;
/* adjust y value for negative rounding, double to int */
if (y < 0)
y -= xtext->fontsize;
line = (y + xtext->pixel_offset) / xtext->fontsize;
ent = gtk_xtext_nth (xtext, line + (int) xtext_adj_get_value (xtext->adj),
&subline);
if (ent == NULL || ent->image == NULL)
return NULL;
image_rows = gtk_xtext_image_rows (xtext, ent);
text_rows = ent->subline_count - image_rows;
if (subline < text_rows)
return NULL;
img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
if (x < img_x || x >= img_x + gdk_pixbuf_get_width (ent->image))
return NULL;
rel_y = (subline - text_rows) * xtext->fontsize +
(y + xtext->pixel_offset) - (line * xtext->fontsize) -
XTEXT_IMAGE_PADDING;
if (rel_y < 0 || rel_y >= gdk_pixbuf_get_height (ent->image))
return NULL;
return ent;
}
static gboolean
gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
{
@@ -2694,8 +2930,15 @@ gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
if (!gtk_xtext_is_selecting (xtext))
{
word = gtk_xtext_get_word (xtext, event_x, event_y, 0, 0, 0, 0);
textentry *word_ent = NULL;
word = gtk_xtext_get_word (xtext, event_x, event_y, &word_ent, 0, 0, 0);
xtext->clicked_ent = word ? word_ent : NULL;
xtext->clicked_image_ent = word ? NULL :
gtk_xtext_image_hit (xtext, event_x, event_y);
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, word ? word : NULL, event);
xtext->clicked_ent = NULL;
xtext->clicked_image_ent = NULL;
}
}
@@ -2722,14 +2965,16 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event)
if (event->button == 3 || event->button == 2) /* right/middle click */
{
word = gtk_xtext_get_word (xtext, x, y, 0, 0, 0, 0);
word = gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0);
if (word)
{
xtext->clicked_ent = ent;
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
word, event);
} else
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
"", event);
xtext->clicked_ent = NULL;
return FALSE;
}
@@ -2958,7 +3203,7 @@ gtk_xtext_selection_get (GtkWidget * widget,
if (!window || !GDK_IS_WINDOW (window))
break;
display = gdk_window_get_display (window);
if (!display)
if (!display || !GDK_IS_X11_DISPLAY (display))
break;
GdkAtom encoding;
gint format;
@@ -2983,6 +3228,29 @@ gtk_xtext_selection_get (GtkWidget * widget,
g_free (stripped);
}
static void
gtk_xtext_clipboard_get (GtkClipboard *clipboard, GtkSelectionData *selection_data,
guint info, gpointer owner)
{
int len;
char *text = gtk_xtext_selection_get_text (GTK_XTEXT (owner), &len);
if (text)
{
gtk_selection_data_set_text (selection_data, text, len);
g_free (text);
}
}
static void
gtk_xtext_clipboard_clear (GtkClipboard *clipboard, gpointer owner)
{
GtkXText *xtext = GTK_XTEXT (owner);
if (xtext->orig_buffer)
gtk_xtext_selection_kill (xtext, NULL);
}
static gboolean
gtk_xtext_scroll (GtkWidget *widget, GdkEventScroll *event)
{
@@ -4107,6 +4375,7 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
unsigned char *str;
int indent, taken, entline, len, y, start_subline;
int emphasis = 0;
int image_rows;
entline = taken = 0;
str = ent->str;
@@ -4177,6 +4446,56 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
}
while (str < ent->str + ent->str_len);
/* draw the inline image on its blank rows below the text. 'subline'
still holds the rows left to skip when the page starts inside the
image area (the text loop above only consumes text sublines). */
image_rows = gtk_xtext_image_rows (xtext, ent);
if (image_rows > 0)
{
int skip = MIN (subline, image_rows);
int rows = image_rows - skip;
int first_row_y = (xtext->fontsize * line) - xtext->pixel_offset;
int drawn = 0;
gboolean render_image = !xtext->dont_render && !xtext->skip_border_fills &&
!xtext->render_hilights_only;
while (rows > 0 && line < lines_max)
{
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
if (render_image)
{
int bg_x = MAX (0, xtext->clip_x);
int bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
if (bg_w > 0)
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
gtk_xtext_draw_sep (xtext, y - xtext->font->ascent);
}
line++;
taken++;
rows--;
drawn++;
}
if (drawn > 0 && render_image)
{
/* this may be the shared draw context, so the clip has to be
undone before anything else is rendered with it */
cairo_t *cr = xtext_create_context (xtext);
int img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
int img_y = first_row_y - (skip * xtext->fontsize) + XTEXT_IMAGE_PADDING;
cairo_save (cr);
cairo_rectangle (cr, 0, first_row_y, win_width + MARGIN,
drawn * xtext->fontsize);
cairo_clip (cr);
gdk_cairo_set_source_pixbuf (cr, ent->image, img_x, img_y);
cairo_paint (cr);
cairo_restore (cr);
cairo_destroy (cr);
}
}
gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline));
return taken;
@@ -4335,6 +4654,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
unsigned char *str;
int indent, len;
int win_width;
int image_rows, i;
g_slist_free (ent->sublines);
ent->sublines = NULL;
@@ -4344,21 +4664,27 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
if (win_width >= ent->indent + ent->str_width)
{
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len));
ent->subline_count = 1;
return ent->subline_count;
}
indent = ent->indent;
str = ent->str;
do
else
{
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
indent = buf->indent;
str += len;
indent = ent->indent;
str = ent->str;
do
{
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
indent = buf->indent;
str += len;
}
while (str < ent->str + ent->str_len);
}
while (str < ent->str + ent->str_len);
/* extra blank rows that make room for an attached inline image;
their offset is str_len, i.e. past the end of the text */
image_rows = gtk_xtext_image_rows (buf->xtext, ent);
for (i = 0; i < image_rows; i++)
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER ((gint) ent->str_len));
ent->subline_count = g_slist_length (ent->sublines);
return ent->subline_count;
@@ -4781,6 +5107,8 @@ gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent)
ent->sublines = NULL;
ent->subline_count = 0;
gtk_xtext_ent_free_image (ent);
g_free (ent);
return visible;
}
@@ -4918,6 +5246,7 @@ gtk_xtext_clear (xtext_buffer *buf, int lines)
while (buf->text_first)
{
next = buf->text_first->next;
gtk_xtext_ent_free_image (buf->text_first);
g_free (buf->text_first);
buf->text_first = next;
}
@@ -5437,6 +5766,9 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp)
ent->next = NULL;
ent->marks = NULL;
ent->subline_count = 0;
ent->image = NULL;
ent->image_full = NULL;
ent->image_url = NULL;
if (ent->indent < MARGIN)
ent->indent = MARGIN; /* 2 pixels is the left margin */
@@ -5957,13 +6289,260 @@ gtk_xtext_buffer_free (xtext_buffer *buf)
gtk_xtext_search_fini (buf);
}
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_buf, buf);
ent = buf->text_first;
while (ent)
{
next = ent->next;
gtk_xtext_ent_free_image (ent);
g_free (ent);
ent = next;
}
g_free (buf);
}
/* ========================================= */
/* ========== INLINE IMAGE API ============= */
/* ========================================= */
/* the entry the last word_click signal was emitted for; only valid while
handling that signal */
textentry *
gtk_xtext_get_clicked_entry (GtkXText *xtext)
{
return xtext->clicked_ent;
}
/* the entry whose inline image the last word_click signal (with a NULL
word) landed on; only valid while handling that signal */
textentry *
gtk_xtext_get_clicked_image_entry (GtkXText *xtext)
{
return xtext->clicked_image_ent;
}
/* check that 'ent' is still an entry of the currently shown buffer; used to
validate entry pointers that were remembered across main loop iterations */
gboolean
gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent)
{
textentry *walk;
if (ent == NULL)
return FALSE;
for (walk = xtext->buffer->text_first; walk; walk = walk->next)
{
if (walk == ent)
return TRUE;
}
return FALSE;
}
gboolean
gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent)
{
return ent != NULL && ent->image != NULL;
}
/* the URL of the entry's attached image, or NULL if it has none */
const char *
gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent)
{
return ent && ent->image ? ent->image_url : NULL;
}
/* larger version of the entry's attached image, for the viewer */
GdkPixbuf *
gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent)
{
if (ent == NULL)
return NULL;
return ent->image_full ? ent->image_full : ent->image;
}
/* the entry's subline count changed (image attached or removed):
redo its layout, fix the scrollbar and redraw */
static void
gtk_xtext_image_relayout (GtkXText *xtext, xtext_buffer *buf, textentry *ent)
{
buf->num_lines -= ent->subline_count;
buf->num_lines += gtk_xtext_lines_taken (buf, ent);
buf->pagetop_ent = NULL;
dontscroll (buf);
if (xtext->buffer == buf)
{
gtk_xtext_adjustment_set (buf, TRUE);
if (buf->scrollbar_down)
xtext_adj_set_value (xtext->adj,
xtext_adj_get_upper (xtext->adj) -
xtext_adj_get_page_size (xtext->adj));
if (gtk_widget_get_realized (GTK_WIDGET (xtext)))
gtk_widget_queue_draw (GTK_WIDGET (xtext));
}
}
/* scale down (never up) to fit within max_width x max_height */
static GdkPixbuf *
gtk_xtext_image_scale_max (GdkPixbuf *pixbuf, int max_width, int max_height)
{
int width = gdk_pixbuf_get_width (pixbuf);
int height = gdk_pixbuf_get_height (pixbuf);
int new_width, new_height;
double scale;
if (width < 1 || height < 1)
return NULL;
if (width <= max_width && height <= max_height)
return g_object_ref (pixbuf);
scale = MIN ((double) max_width / width, (double) max_height / height);
new_width = MAX (1, (int) (width * scale));
new_height = MAX (1, (int) (height * scale));
return gdk_pixbuf_scale_simple (pixbuf, new_width, new_height,
GDK_INTERP_BILINEAR);
}
/* scale down (never up) to fit the usual text area */
static GdkPixbuf *
gtk_xtext_image_scale (xtext_buffer *buf, GdkPixbuf *pixbuf)
{
int max_width = XTEXT_IMAGE_MAX_WIDTH;
int avail;
avail = buf->window_width - buf->indent - MARGIN - XTEXT_IMAGE_PADDING * 2;
if (avail >= 64 && avail < max_width)
max_width = avail;
return gtk_xtext_image_scale_max (pixbuf, max_width, XTEXT_IMAGE_MAX_HEIGHT);
}
/* register an asynchronous image load for 'ent'. Returns a handle to pass
to gtk_xtext_image_load_finish(), or 0 if the entry already has an image
or a load underway. */
guint
gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url)
{
xtext_image_pending *pending;
GSList *list;
if (ent == NULL || ent->image != NULL)
return 0;
for (list = image_pending_list; list; list = list->next)
{
pending = list->data;
if (pending->ent == ent)
return 0;
}
pending = g_new0 (xtext_image_pending, 1);
pending->id = image_pending_next_id++;
if (image_pending_next_id == 0) /* skip 0 on wrap, it means failure */
image_pending_next_id = 1;
pending->xtext = xtext;
pending->buf = xtext->buffer;
pending->ent = ent;
pending->url = g_strdup (url);
image_pending_list = g_slist_prepend (image_pending_list, pending);
return pending->id;
}
/* complete a load started with gtk_xtext_image_load_begin(). pixbuf may be
NULL to abandon the load (e.g. download failed). Returns TRUE if the
image was attached, FALSE if it was dropped (failure, or the entry is
gone). The caller keeps ownership of the pixbuf. */
gboolean
gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf)
{
xtext_image_pending *pending = NULL;
GSList *list;
GdkPixbuf *scaled;
GtkXText *xtext;
xtext_buffer *buf;
textentry *ent;
if (handle == 0)
return FALSE;
for (list = image_pending_list; list; list = list->next)
{
xtext_image_pending *walk = list->data;
if (walk->id == handle)
{
pending = walk;
break;
}
}
if (pending == NULL) /* entry, buffer or widget died meanwhile */
return FALSE;
xtext = pending->xtext;
buf = pending->buf;
ent = pending->ent;
image_pending_list = g_slist_remove (image_pending_list, pending);
if (pixbuf == NULL || ent->image != NULL)
{
g_free (pending->url);
g_free (pending);
return FALSE;
}
scaled = gtk_xtext_image_scale (buf, pixbuf);
if (scaled == NULL)
{
g_free (pending->url);
g_free (pending);
return FALSE;
}
ent->image = scaled;
ent->image_full = gtk_xtext_image_scale_max (pixbuf,
XTEXT_IMAGE_FULL_MAX_WIDTH, XTEXT_IMAGE_FULL_MAX_HEIGHT);
ent->image_url = pending->url; /* transferred */
g_free (pending);
gtk_xtext_image_relayout (xtext, buf, ent);
return TRUE;
}
/* remove an entry's inline image again. Returns FALSE if it had none. */
gboolean
gtk_xtext_image_remove (GtkXText *xtext, textentry *ent)
{
if (ent == NULL || ent->image == NULL)
return FALSE;
g_object_unref (ent->image);
ent->image = NULL;
if (ent->image_full)
{
g_object_unref (ent->image_full);
ent->image_full = NULL;
}
g_free (ent->image_url);
ent->image_url = NULL;
gtk_xtext_image_relayout (xtext, xtext->buffer, ent);
return TRUE;
}
+19
View File
@@ -192,6 +192,7 @@ struct _GtkXText
int hilight_end;
time_t tooltip_stamp;
unsigned int tooltip_stamp_set:1;
char *tooltip_url; /* URL shown while hovering a placeholder */
guint16 fontwidth[128]; /* each char's width, only the ASCII ones */
@@ -254,6 +255,9 @@ struct _GtkXText
unsigned int force_render:1;
unsigned int color_paste:1; /* CTRL was pressed when selection finished */
textentry *clicked_ent; /* entry under the last word_click, or NULL */
textentry *clicked_image_ent; /* entry whose inline image was clicked */
/* settings/prefs */
unsigned int auto_indent:1;
unsigned int thinline:1;
@@ -309,4 +313,19 @@ void gtk_xtext_buffer_free (xtext_buffer *buf);
void gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render);
void gtk_xtext_copy_selection (GtkXText *xtext);
/* inline image support. A textentry can have one image attached, which is
* rendered below its text. Loading is asynchronous: begin() registers the
* target entry and returns a handle, finish() attaches the downloaded pixbuf
* if the entry still exists. The handle stays valid even if the entry, its
* buffer or the widget are destroyed in the meantime. */
textentry *gtk_xtext_get_clicked_entry (GtkXText *xtext);
textentry *gtk_xtext_get_clicked_image_entry (GtkXText *xtext);
gboolean gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent);
gboolean gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent);
const char *gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent);
GdkPixbuf *gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent);
guint gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url);
gboolean gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf);
gboolean gtk_xtext_image_remove (GtkXText *xtext, textentry *ent);
#endif
+13
View File
@@ -467,6 +467,9 @@ static const GOptionEntry gopt_entries[] =
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
#ifdef __linux__
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
#endif
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel URL"), "URL"},
@@ -475,6 +478,16 @@ static const GOptionEntry gopt_entries[] =
{NULL}
};
#ifdef __linux__
void
fe_root_warning (void)
{
fputs ("ZoiteChat refuses to run as root on Linux.\n"
"Run it as a normal user, or pass --run-as-root to override this protection.\n",
stderr);
}
#endif
int
fe_args (int argc, char *argv[])
{
+2
View File
@@ -90,6 +90,7 @@
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\index.theme" />
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\**\*.*" />
<PythonAdditiveApis Include="..\..\plugins\python\_zoitechat_*.py" />
</ItemGroup>
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
@@ -115,6 +116,7 @@
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="@(PythonAdditiveApis)" DestinationFolder="$(ZoiteChatRel)\python" Condition="('$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true') and '@(PythonAdditiveApis)' != ''" />
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />